nordabiz/blueprints/public/routes.py
Maciej Pienczyn 75b018808a
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions
docs: add release notes v1.41.0, v1.42.0, v1.43.0
v1.41.0 (Mar 7): "Nowe" badges on dashboard, event access labels, 4 events grid
v1.42.0 (Mar 11): messaging overhaul - attachments, autocomplete, read receipts,
  thread view, color-coded status pills, redesigned inbox/sent
v1.43.0 (Mar 12): "Co nowego" banner on homepage, privacy policy & ToS pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 07:49:03 +01:00

1757 lines
89 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, date
from flask import render_template, request, redirect, url_for, flash, session, current_app, Response
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,
MembershipApplication,
Announcement,
ForumTopic,
Classified,
UserNotification,
UserCompany,
)
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 with public company tiles
db = SessionLocal()
try:
companies = db.query(Company).filter_by(status='active').order_by(Company.name).all()
total_companies = len(companies)
total_categories = db.query(Category).count()
# Load social media for all companies in one query
company_social = {}
if companies:
social_records = db.query(CompanySocialMedia).filter(
CompanySocialMedia.company_id.in_([c.id for c in companies])
).all()
for sm in social_records:
company_social.setdefault(sm.company_id, []).append(sm)
return render_template(
'landing.html',
companies=companies,
company_social=company_social,
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 wydarzenia (dla bannera "Kto weźmie udział?")
all_upcoming = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today()
).order_by(NordaEvent.event_date.asc()).all()
upcoming_events = []
for event in all_upcoming:
if event.can_user_view(current_user):
registered = db.query(EventAttendee).filter(
EventAttendee.event_id == event.id,
EventAttendee.user_id == current_user.id
).first() is not None
can_attend = event.can_user_attend(current_user)
upcoming_events.append({
'event': event,
'user_registered': registered,
'user_can_attend': can_attend,
})
if len(upcoming_events) >= 4:
break
# Backward compat — next_event used by other parts
next_event = upcoming_events[0]['event'] if upcoming_events else None
# ZOPK Knowledge facts — admin only widget
zopk_facts = []
if current_user.is_admin:
try:
from database import ZOPKKnowledgeFact, ZOPKNews
zopk_facts = db.query(ZOPKKnowledgeFact).join(ZOPKNews).filter(
ZOPKKnowledgeFact.confidence_score >= 0.5
).order_by(func.random()).limit(3).all()
except Exception:
pass
# Sprawdź czy użytkownik ma deklarację członkowską w toku
pending_application = None
if not current_user.is_norda_member and not current_user.company_id:
pending_application = db.query(MembershipApplication).filter(
MembershipApplication.user_id == current_user.id,
MembershipApplication.status.in_(['draft', 'submitted', 'under_review', 'pending_user_approval', 'changes_requested'])
).first()
# Latest release for "Co nowego" banner
all_releases = _get_releases()
latest_release = all_releases[0] if all_releases else 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,
upcoming_events=upcoming_events,
pending_application=pending_application,
zopk_facts=zopk_facts,
latest_release=latest_release
)
finally:
db.close()
@bp.route('/company/<int:company_id>')
def company_detail(company_id):
"""Company detail page - requires login and NORDA membership"""
# Sprawdź czy użytkownik jest zalogowany
if not current_user.is_authenticated:
flash('Zaloguj się, aby zobaczyć szczegóły firmy.', 'warning')
return redirect(url_for('auth.login'))
# Sprawdź czy użytkownik jest członkiem NORDA (ma firmę lub flagę is_norda_member)
if not current_user.is_norda_member and not current_user.company_id:
flash('Dostęp do katalog firm jest dostępny tylko dla członków Izby NORDA. Złóż deklarację członkowską, aby uzyskać pełny dostęp.', 'info')
return redirect(url_for('membership.apply'))
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 all website analyses sorted by audit date (latest first)
all_analyses = db.query(CompanyWebsiteAnalysis).filter_by(
company_id=company_id
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).all()
# Build dict: company_website_id → analysis (latest first wins)
analyses_by_website = {}
website_analysis = None # backward compat: keep "main" analysis
for a in all_analyses:
if a.company_website_id and a.company_website_id not in analyses_by_website:
analyses_by_website[a.company_website_id] = a
if website_analysis is None:
website_analysis = a # first = latest = main analysis
# 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 portal users linked to this company (for "Osoby kontaktowe" section)
company_users = db.query(UserCompany).filter_by(
company_id=company_id
).join(User, UserCompany.user_id == User.id).filter(
User.is_active == True
).order_by(
UserCompany.role.desc(), # MANAGER first, then EMPLOYEE
User.name
).all()
# Eager-load user attributes before session close
for uc in company_users:
_ = uc.user.name, uc.user.email, uc.user.phone, uc.user.is_norda_member
_ = uc.user.privacy_show_phone, uc.user.privacy_show_email
_ = uc.user.contact_prefer_email, uc.user.contact_prefer_phone
_ = uc.user.contact_prefer_portal, uc.user.contact_note
# 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 (user with company edit rights)
can_enrich = False
can_edit_profile = False
company_managers = []
if current_user.is_authenticated:
can_enrich = current_user.can_edit_company(company.id)
can_edit_profile = current_user.can_manage_company(company.id)
# If user is a member but not manager, load managers for contact modal
if not can_edit_profile:
is_company_member = any(
assoc.company_id == company.id
for assoc in (current_user.company_associations or [])
) or current_user.company_id == company.id
if is_company_member:
company_managers = db.query(User).join(UserCompany).filter(
UserCompany.company_id == company.id,
UserCompany.role == 'MANAGER'
).all()
for m in company_managers:
_ = m.name, m.email # force-load before session close
# For child brands — inherit NIP from parent for display/enrichment
effective_nip = company.nip
if not effective_nip and company.parent_company_id:
parent = db.query(Company).filter_by(id=company.parent_company_id).first()
if parent:
effective_nip = parent.nip
return render_template('company_detail.html',
company=company,
company_id=company.id, # For analytics conversion tracking
effective_nip=effective_nip,
maturity_data=maturity_data,
website_analysis=website_analysis,
analyses_by_website=analyses_by_website,
quality_data=quality_data,
events=events,
website_content=website_content,
ai_insights=ai_insights,
social_media=social_media,
contacts=contacts,
recommendations=recommendations,
people=people,
company_users=company_users,
gbp_audit=gbp_audit,
it_audit=it_audit,
pkd_codes=pkd_codes,
can_enrich=can_enrich,
can_edit_profile=can_edit_profile,
company_managers=company_managers,
is_admin=current_user.is_authenticated and current_user.is_admin
)
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)
search_query_id = None
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()
search_query_id = search_query.id
except Exception as e:
logger.error(f"Search logging error: {e}")
db.rollback()
search_query_id = None
# 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),
search_query_id=search_query_id
)
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()
# New stats
unread_notifications = db.query(UserNotification).filter(
UserNotification.user_id == current_user.id,
UserNotification.is_read == False
).count()
upcoming_events_count = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today()
).count()
user_forum_topics_count = db.query(ForumTopic).filter(
ForumTopic.author_id == current_user.id,
ForumTopic.is_deleted == False
).count()
# Check for membership application status
has_pending_application = False
has_draft_application = False
pending_application = None
try:
from database import MembershipApplication
pending_application = db.query(MembershipApplication).filter(
MembershipApplication.user_id == current_user.id,
MembershipApplication.status.in_(['submitted', 'under_review', 'changes_requested'])
).first()
has_pending_application = pending_application is not None
if not has_pending_application:
draft = db.query(MembershipApplication).filter(
MembershipApplication.user_id == current_user.id,
MembershipApplication.status == 'draft'
).first()
has_draft_application = draft is not None
except Exception:
pass # MembershipApplication table may not exist yet
# Load user's company associations (multi-company support)
from sqlalchemy.orm import joinedload
user_companies = db.query(UserCompany).options(
joinedload(UserCompany.company)
).filter_by(
user_id=current_user.id
).order_by(UserCompany.is_primary.desc(), UserCompany.created_at.asc()).all()
# Force-load company names before session closes
for uc in user_companies:
_ = uc.company.name if uc.company else None
# Managers map for companies where user is EMPLOYEE (for edit permission modal)
company_managers_map = {}
employee_company_ids = [uc.company_id for uc in user_companies if uc.role == 'EMPLOYEE']
if employee_company_ids:
managers = db.query(User, UserCompany.company_id).join(UserCompany).filter(
UserCompany.company_id.in_(employee_company_ids),
UserCompany.role == 'MANAGER'
).all()
for mgr, cid in managers:
company_managers_map.setdefault(cid, []).append({'name': mgr.name or 'Brak imienia', 'email': mgr.email or ''})
# Onboarding progress widget
from utils.onboarding import compute_onboarding_steps
onboarding = compute_onboarding_steps(current_user, user_companies, current_app.root_path)
# Widget 1: Upcoming events (3 nearest future events)
upcoming_events = db.query(NordaEvent).filter(
NordaEvent.event_date >= date.today()
).order_by(NordaEvent.event_date.asc()).limit(3).all()
# Batch RSVP lookup for current user + attendee counts
user_event_ids = set()
event_attendee_counts = {}
if upcoming_events:
event_ids = [e.id for e in upcoming_events]
rsvps = db.query(EventAttendee.event_id).filter(
EventAttendee.event_id.in_(event_ids),
EventAttendee.user_id == current_user.id,
EventAttendee.status == 'confirmed'
).all()
user_event_ids = {r[0] for r in rsvps}
# Count confirmed attendees per event
from sqlalchemy import func
counts = db.query(EventAttendee.event_id, func.count(EventAttendee.id)).filter(
EventAttendee.event_id.in_(event_ids),
EventAttendee.status == 'confirmed'
).group_by(EventAttendee.event_id).all()
event_attendee_counts = {eid: cnt for eid, cnt in counts}
# Widget 2: Recent announcements (3 latest published, pinned first, not expired)
recent_announcements = db.query(Announcement).filter(
Announcement.status == 'published',
or_(Announcement.expires_at == None, Announcement.expires_at > datetime.now())
).order_by(Announcement.is_pinned.desc(), Announcement.published_at.desc()).limit(3).all()
# Widget 3: Recent forum topics (5 latest active)
recent_forum_topics = db.query(ForumTopic).filter(
ForumTopic.is_deleted == False
).order_by(ForumTopic.updated_at.desc()).limit(5).all()
# Widget 4: Recent classifieds (3 active, not test, not expired)
recent_classifieds = db.query(Classified).filter(
Classified.is_active == True,
Classified.is_test == False,
or_(Classified.expires_at == None, Classified.expires_at > datetime.now())
).order_by(Classified.created_at.desc()).limit(3).all()
# Widget 5: New companies (3 newest)
new_companies = db.query(Company).order_by(
Company.created_at.desc()
).limit(3).all()
# Admin KPI widget (only computed for admins)
admin_kpi = None
if current_user.can_access_admin_panel():
from sqlalchemy import func as sqlfunc
from database import UserSession, PageView, SecurityAlert
from datetime import timedelta as td
now = datetime.now()
week_ago = now - td(days=7)
prev_week = now - td(days=14)
# Active members (logged in last 7d)
active_7d = db.query(sqlfunc.count(sqlfunc.distinct(UserSession.user_id))).filter(
UserSession.started_at >= week_ago,
UserSession.user_id.isnot(None),
UserSession.is_bot == False
).scalar() or 0
# Total registered users
total_users = db.query(sqlfunc.count(User.id)).filter(User.is_active == True).scalar() or 0
# Sessions this week
sessions_7d = db.query(sqlfunc.count(UserSession.id)).filter(
UserSession.started_at >= week_ago,
UserSession.is_bot == False
).scalar() or 0
# Active problems (security alerts + locked accounts)
active_problems = db.query(sqlfunc.count(SecurityAlert.id)).filter(
SecurityAlert.created_at >= week_ago
).scalar() or 0
# Never logged in
never_logged = db.query(sqlfunc.count(User.id)).filter(
User.is_active == True,
User.last_login.is_(None)
).scalar() or 0
admin_kpi = {
'active_users': active_7d,
'total_users': total_users,
'active_pct': round(active_7d / total_users * 100) if total_users > 0 else 0,
'sessions': sessions_7d,
'problems': active_problems,
'never_logged': never_logged,
}
return render_template(
'dashboard.html',
admin_kpi=admin_kpi,
conversations=conversations,
total_conversations=total_conversations,
total_messages=total_messages,
has_pending_application=has_pending_application,
has_draft_application=has_draft_application,
pending_application=pending_application,
user_companies=user_companies,
unread_notifications=unread_notifications,
upcoming_events_count=upcoming_events_count,
user_forum_topics_count=user_forum_topics_count,
upcoming_events=upcoming_events,
user_event_ids=user_event_ids,
event_attendee_counts=event_attendee_counts,
recent_announcements=recent_announcements,
recent_forum_topics=recent_forum_topics,
recent_classifieds=recent_classifieds,
new_companies=new_companies,
company_managers_map=company_managers_map,
onboarding=onboarding
)
finally:
db.close()
def _get_releases():
"""Return list of all release notes entries."""
return [
{
'version': 'v1.43.0',
'date': '12 marca 2026',
'badges': ['new'],
'new': [
'<strong>Nowości na stronie głównej</strong> - pod nagłówkiem katalogu firm wyświetla się informacja o najnowszych zmianach na platformie z linkiem do pełnej listy',
'<strong>Polityka prywatności i Regulamin</strong> - platforma ma teraz własne dokumenty prawne dostępne w stopce strony',
],
},
{
'version': 'v1.42.0',
'date': '11 marca 2026',
'badges': ['new', 'improve'],
'new': [
'★ <strong>Załączniki do wiadomości</strong> - do każdej wiadomości prywatnej można dołączyć pliki (dokumenty, zdjęcia, PDF) zarówno przy wysyłaniu nowej wiadomości, jak i przy odpowiedzi',
'★ <strong>Wyszukiwanie odbiorcy</strong> - zamiast rozwijanej listy wystarczy zacząć wpisywać imię, nazwisko lub nazwę firmy, a portal podpowie pasujące osoby',
'<strong>Podgląd profilu odbiorcy</strong> - po wybraniu odbiorcy wyświetla się karta z jego danymi i firmą, dzięki czemu wiadomo do kogo trafia wiadomość',
'<strong>Potwierdzenie odczytania</strong> - w skrzynce wysłanych widoczne jest, czy odbiorca przeczytał wiadomość',
'<strong>Historia korespondencji</strong> - po otwarciu wiadomości widoczny jest cały wątek rozmowy z daną osobą w jednym miejscu',
],
'improve': [
'<strong>Kolorowe statusy wiadomości</strong> - wiadomości nowe, wysłane i przeczytane oznaczone są kolorowymi etykietami dla łatwiejszego przeglądania',
'<strong>Nowy wygląd skrzynki</strong> - skrzynka odbiorcza i wysłane zostały przebudowane dla lepszej czytelności na komputerze i telefonie',
],
},
{
'version': 'v1.41.0',
'date': '7 marca 2026',
'badges': ['new', 'improve'],
'new': [
'<strong>Oznaczenie „Nowe"</strong> - na pulpicie widoczne jest, które wydarzenia i ogłoszenia pojawiły się od ostatniego logowania',
'<strong>Oznaczenia dostępu do wydarzeń</strong> - wydarzenia z ograniczonym dostępem (tylko dla Rady lub ukryte) mają widoczne etykiety',
],
'improve': [
'<strong>Więcej wydarzeń na stronie głównej</strong> - zamiast dwóch wyświetlają się cztery najbliższe wydarzenia w przejrzystej siatce',
],
},
{
'version': 'v1.40.0',
'date': '7 marca 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Sekcja Aktualności</strong> - ogłoszenia i informacje dla członków Izby z wyróżnionymi wydarzeniami, zdjęciami i załącznikami',
'<strong>Ulepszona sztuczna inteligencja</strong> - NordaGPT korzysta teraz z najnowszej generacji modeli Google Gemini 3.1 z lepszym rozumieniem pytań',
],
'improve': [
'<strong>Szybsze audyty SEO</strong> - analiza stron internetowych firm korzysta z wydajniejszego parsera HTML',
'<strong>Płynniejsze korzystanie z portalu</strong> - automatyczne odświeżanie powiadomień i wiadomości nie wpływa już na limit zapytań',
],
'fix': [
'<strong>Kalendarz</strong> - naprawiono błąd przy wpisaniu nieprawidłowego roku w adresie strony',
'<strong>Audyt SEO</strong> - naprawiono zapis danych o wykorzystaniu zapytań do Google PageSpeed',
],
},
{
'version': 'v1.39.0',
'date': '23 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Powiadomienia e-mail o wiadomościach</strong> - po otrzymaniu wiadomości prywatnej przychodzi e-mail z podglądem treści i linkiem do odpowiedzi',
'<strong>Zarządzanie powiadomieniami</strong> - w ustawieniach prywatności można włączyć lub wyłączyć powiadomienia e-mail o nowych wiadomościach',
'★ <strong>Instalacja aplikacji na telefonie</strong> - na Androidzie wystarczy jedno kliknięcie „Zainstaluj", na iPhonie instrukcja krok po kroku z obrazkami',
],
'improve': [
'<strong>Klikalne linki na forum</strong> - adresy stron internetowych wklejone we wpisach na forum automatycznie stają się klikalnymi linkami',
'<strong>Godziny otwarcia na żywo</strong> - status „otwarte/zamknięte" na profilu firmy obliczany jest na bieżąco na podstawie aktualnej godziny',
],
'fix': [
'<strong>Formatowanie wpisów na forum</strong> - poprawiono zbyt duże odstępy między wierszami i prawidłowe wyświetlanie list',
'<strong>Podwójne wysyłanie odpowiedzi</strong> - naprawiono błąd umożliwiający przypadkowe podwójne wysłanie odpowiedzi na forum',
],
},
{
'version': 'v1.38.0',
'date': '22 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Osoby kontaktowe na profilu firmy</strong> - na profilu każdej firmy widoczne są osoby powiązane z firmą wraz z danymi kontaktowymi i możliwością wysłania wiadomości',
'<strong>Wiadomości w menu głównym</strong> - nowy skrót do wiadomości prywatnych w górnym menu z licznikiem nieprzeczytanych',
'<strong>Powiadomienia o nowych wiadomościach</strong> - po otrzymaniu wiadomości prywatnej pojawia się powiadomienie w dzwoneczku',
],
'improve': [
'<strong>Karta kontaktowa na profilu firmy</strong> - wyraźne przyciski do wysłania e-maila i wiadomości prywatnej na portalu',
'<strong>Oznaczenie członków Norda</strong> - osoby powiązane z firmami członkowskimi mają widoczny znaczek „Norda"',
'<strong>Podgląd reakcji na forum</strong> - po najechaniu na reakcję widoczna lista osób, które zareagowały',
],
'fix': [
'<strong>Podgląd profilu na forum</strong> - naprawiono wyświetlanie statystyk po najechaniu na autora wpisu',
],
},
{
'version': 'v1.37.0',
'date': '21 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Wyszukiwanie stron internetowych firm</strong> - automatyczne znajdowanie stron WWW dla firm, które ich nie mają w katalogu',
'<strong>Audyt SEO portalu</strong> - panel do sprawdzania jakości SEO samej platformy Norda Biznes z historią wyników',
'<strong>Firmy na stronie głównej</strong> - kafelki firm członkowskich widoczne na stronie startowej portalu',
'<strong>Nowy wygląd e-maili</strong> - ujednolicony, profesjonalny szablon wszystkich wiadomości e-mail z portalu',
],
'improve': [
'<strong>Jakość danych firm</strong> - nowy panel z oceną kompletności danych, podpowiedziami i hurtowym uzupełnianiem informacji',
'<strong>Poprawne adresy stron firm</strong> - linki do stron internetowych działają teraz prawidłowo we wszystkich przypadkach',
],
'fix': [
'<strong>Poprawny adres i telefon Izby</strong> - zaktualizowano dane kontaktowe na stronie portalu',
'<strong>Linki do Facebooka</strong> - naprawiono wyświetlanie odnośnika do strony Norda Biznes na Facebooku',
],
},
{
'version': 'v1.36.0',
'date': '20 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Wykresy analityki postów</strong> - 6 wykresów pokazujących zaangażowanie, aktywność publikacji, typy postów i najlepsze dni/godziny',
'<strong>Ranking Top 5 postów</strong> - najlepiej performujące posty z Facebooka z linkami do oryginału',
'<strong>Paginacja listy postów</strong> - wybór wyświetlania 10, 20, 50, 100 lub wszystkich postów na stronie',
'<strong>Zwijane sekcje</strong> - każdą sekcję można zwinąć lub rozwinąć, a ustawienie jest zapamiętywane między wizytami',
'<strong>Przycisk "Zaktualizuj dane"</strong> - ponowne pobranie wszystkich danych firmy z rejestrów, audytów i mediów społecznościowych',
],
'improve': [
'<strong>Audyt wizytówki Google</strong> - czytelniejszy widok wyników i weryfikacja czy znaleziona wizytówka należy do szukanej firmy',
'<strong>Wykrywanie profili społecznościowych</strong> - naprawiono zapisywanie znalezionych profili Facebook, Instagram, YouTube, LinkedIn i TikTok',
'<strong>Pobieranie danych z CEIDG</strong> - poprawiono wyszukiwanie jednoosobowych firm w rejestrze CEIDG z pełnym mapowaniem pól',
'<strong>Automatyczne wykresy</strong> - wykresy wyświetlają się natychmiast po wejściu na stronę bez konieczności klikania',
'<strong>Nowa kolejność sekcji</strong> - wykresy na górze, posty z aplikacji w środku, posty z Facebooka na dole',
],
'fix': [
'<strong>Logo firm w formacie SVG</strong> - naprawiono wyświetlanie logo zapisanych w formacie wektorowym',
'<strong>Pobieranie logo</strong> - naprawiono błąd uniemożliwiający pobranie logo z niektórych stron internetowych',
'<strong>Duplikaty postów z Facebooka</strong> - naprawiono problem z pobieraniem tych samych postów wielokrotnie',
],
},
{
'version': 'v1.35.0',
'date': '19 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Moduł Social Media</strong> - nowy moduł do zarządzania obecnością firmy w mediach społecznościowych',
'<strong>Tworzenie postów z pomocą AI</strong> - generowanie treści i hashtagów przez sztuczną inteligencję',
'<strong>Wybór tonu komunikacji</strong> - 5 stylów pisania od autentycznego przez ekspercki po inspirujący',
'<strong>Wybór silnika AI</strong> - możliwość wyboru modelu do generowania treści',
'<strong>Publikowanie na Facebooku</strong> - tworzenie i publikowanie postów bezpośrednio z platformy',
'<strong>Widoczność postów</strong> - przełączanie między trybem testowym a publicznym dla każdego posta',
'<strong>Posty z Facebooka ze statystykami</strong> - wyświetlanie polubień, komentarzy, udostępnień i reakcji',
'<strong>Diagram cyklu życia posta</strong> - wizualizacja procesu od szkicu po publikację',
'<strong>Synchronizacja danych z Facebooka</strong> - automatyczne pobieranie kategorii, telefonu i adresu ze strony firmowej',
'<strong>Integracje dla kierowników</strong> - strona integracji dostępna dla osób z rolą Zarządzający',
],
'improve': [
'<strong>Linki social media w postach AI</strong> - generowane treści uwzględniają profile firmy w mediach społecznościowych',
'<strong>Monitorowanie kosztów AI</strong> - informacja o zużyciu zasobów przy generowaniu treści',
'<strong>Liczba uczestników przy wydarzeniach</strong> - widoczna na pulpicie obok nadchodzących wydarzeń',
'<strong>Klikalna karta "Twoja firma"</strong> - na pulpicie prowadzi do profilu firmy',
'<strong>Ochrona adresów Facebook</strong> - identyfikator numeryczny nie nadpisuje czytelnego adresu firmy',
],
'fix': [
'<strong>Połączenie z Facebookiem</strong> - naprawiono przechowywanie danych autoryzacji dla niezawodnego publikowania',
'<strong>Ponowna próba publikacji</strong> - nieudane posty można opublikować ponownie',
'<strong>Diagnostyka połączenia z Facebookiem</strong> - dodano informacje pomagające rozwiązać problemy z połączeniem',
'<strong>Opisy wydarzeń w kalendarzu</strong> - poprawiono wyświetlanie formatowanego tekstu w widoku listy',
],
},
{
'version': 'v1.34.0',
'date': '18 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Edytor z podglądem na żywo</strong> - edycja opisów firmy z formatowaniem tekstu i natychmiastowym podglądem wyniku',
'★ <strong>Automatyczne pobieranie logo</strong> - system pobiera logo ze strony internetowej firmy i proponuje kandydatów z oceną jakości',
'<strong>Galeria kandydatów na logo</strong> - porównanie wielu propozycji logo przed wyborem najlepszego',
'<strong>Notatki administracyjne o firmach</strong> - wewnętrzne notatki widoczne tylko dla administratorów',
'<strong>Kontrola ról nowych użytkowników</strong> - nowi użytkownicy otrzymują ograniczoną rolę do czasu zatwierdzenia przez admina',
'<strong>Zmiana roli użytkownika z listy</strong> - administrator może zmieniać rolę bezpośrednio w panelu użytkowników',
],
'improve': [
'<strong>Nowy wygląd strony edycji firmy</strong> - czytelniejszy układ z kolorową organizacją sekcji',
'<strong>Porównanie logo przed zmianą</strong> - podgląd dotychczasowego i nowego logo obok siebie',
'<strong>Uprawnienia kierowników</strong> - osoby zarządzające automatycznie otrzymują pełne uprawnienia przy tworzeniu konta',
'<strong>Powiadomienie o nowej rejestracji</strong> - naprawiono wysyłanie emaila z alertem o oczekujących zatwierdzeniach',
],
'fix': [
'<strong>Pobieranie danych z CEIDG</strong> - naprawiono pobieranie i mapowanie pól z rejestru jednoosobowych firm',
'<strong>Audyt SEO</strong> - poprawna obsługa wolnych połączeń i stron, które nie istnieją',
'<strong>Wyświetlanie formatowanych opisów firm</strong> - opisy z edytora wyświetlają się poprawnie na profilu firmy',
'<strong>Audyt social media</strong> - usunięto źródło generujące zbyt wiele fałszywych wyników',
],
},
{
'version': 'v1.33.0',
'date': '17 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Wiele stron internetowych na profilu firmy</strong> - możliwość dodania do 5 stron z oznaczeniem typu (główna, e-commerce, blog, portfolio)',
'<strong>Kolorowe przyciski typów stron</strong> - każdy typ strony ma inny kolor z podpowiedzią po najechaniu',
'<strong>Osobne wyniki audytu dla każdej strony</strong> - karta wyników SEO wyświetlana oddzielnie dla każdej strony firmy',
'<strong>Kontrola widoczności sekcji profilu</strong> - właściciel firmy może ukrywać lub pokazywać poszczególne sekcje (rejestry, social media, SEO)',
'<strong>Szczegółowa kontrola sekcji rejestrów</strong> - oddzielne przełączniki dla KRS, CEIDG i Białej Listy VAT',
'<strong>Rozpoznawanie typu profilu LinkedIn</strong> - audyt rozróżnia profil firmowy od osobistego',
],
'improve': [
'<strong>Dokładniejsze wykrywanie LinkedIn</strong> - lepsze rozpoznawanie profili firmowych w wynikach wyszukiwania',
'<strong>Pasek postępu audytu social media</strong> - dodatkowy krok wyszukiwania widoczny w pasku postępu',
],
'fix': [
'<strong>Usuwanie stron internetowych firmy</strong> - naprawiono usuwanie wszystkich stron przy zapisywaniu zmian',
'<strong>Rozdzielanie połączonych adresów</strong> - system prawidłowo rozdziela adresy zapisane w jednym polu',
'<strong>Wykrywanie LinkedIn</strong> - poprawiono dokładność sprawdzania profili LinkedIn',
],
},
{
'version': 'v1.32.0',
'date': '14 lutego 2026',
'badges': ['improve', 'fix'],
'improve': [
'<strong>Powiadomienia na telefonie</strong> - dropdown wyświetla się jako panel od dołu ekranu zamiast być obcinanym',
'<strong>Dodawanie zdjęć na forum z telefonu</strong> - przycisk "Dodaj zdjęcie z galerii" zamiast "Ctrl+V" na urządzeniach mobilnych',
],
'fix': [
'<strong>Usuwanie odpowiedzi na forum</strong> - naprawiono błąd uniemożliwiający usunięcie odpowiedzi, które były już przeczytane przez innych',
'<strong>Wysyłanie załączników z telefonu</strong> - naprawiono problem z nieprzesyłaniem zdjęć wybranych z galerii na urządzeniach mobilnych',
'<strong>Pulpit użytkownika</strong> - naprawiono błąd 500 przy wyświetlaniu dashboardu',
],
},
{
'version': 'v1.31.0',
'date': '13 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Widget postępu wdrożenia na pulpicie</strong> - widoczny stan konfiguracji firmy po zalogowaniu',
'<strong>Zdjęcia w wydarzeniach</strong> - wydarzenia mogą zawierać zdjęcia w opisie',
'<strong>Nowa firma: UNIMOT Energia i Gaz</strong> - logotyp nowej firmy członkowskiej',
'<strong>Weryfikacja certyfikatu SSL w audycie SEO</strong> - sprawdzanie ważności certyfikatu strony',
],
'improve': [
'<strong>Czytelniejsze opisy wydarzeń</strong> - poprawione formatowanie tekstu i wyświetlanie obrazów w kalendarzu',
'<strong>Walidacja NIP przy rejestracji</strong> - sprawdzanie sumy kontrolnej numeru NIP',
],
'fix': [
'<strong>Audyt SEO</strong> - naprawiono ładowanie kluczy API przy uruchamianiu audytu',
],
},
{
'version': 'v1.30.0',
'date': '12 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>3 najbliższe wydarzenia na stronie głównej</strong> - szybki podgląd kalendarza bez wchodzenia w szczegóły',
'<strong>Nowe firmy: Alter Energy, Fiume Studio</strong> - logotypy nowych firm członkowskich',
'<strong>Marki zależne dziedziczą NIP</strong> - marki podrzędne automatycznie wyświetlają NIP firmy macierzystej',
],
'improve': [
'<strong>Lepsza detekcja profili Facebook w audycie</strong> - wykluczenie pixeli śledzących i parametrów Meta z wyników',
],
'fix': [
'<strong>Synchronizacja użytkownik-firma</strong> - naprawiono powiązania w tabeli user_companies i nazwy pól kalendarza',
],
},
{
'version': 'v1.29.0',
'date': '9 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Analiza planu rozwoju Izby przez AI</strong> - sztuczna inteligencja ocenia postęp realizacji strategii i wskazuje braki',
'<strong>Panel wiedzy ZOPK na stronie głównej</strong> - wybrane wpisy bazy wiedzy widoczne bez wchodzenia w sekcję ZOPK',
'<strong>Przyciski zarządzania na stronach ZOPK</strong> - administratorzy mogą edytować treści bezpośrednio ze strony publicznej',
'<strong>Baza wiedzy ZOPK w czasie rzeczywistym</strong> - postęp wyszukiwania i ekstrakcji widoczny na żywo w przeglądarce',
'<strong>4-etapowy panel zarządzania bazą wiedzy ZOPK</strong> - workflow od wyszukiwania przez scraping po ekstrakcję',
'<strong>Rozszerzenie Google API do ~100%</strong> - GBP Performance i Google Search Console w pełni zintegrowane',
'<strong>Kontakt z przełożonym dla pracowników</strong> - pracownik widzi dane kontaktowe osoby zarządzającej firmą',
'<strong>Pełna matryca uprawnień w panelu dostępu</strong> - widoczne wszystkie uprawnienia dla każdej roli',
'<strong>Bezpośrednie ustawianie hasła przez admina</strong> - bez konieczności wysyłania linku resetującego',
'<strong>Kolumna ostatniego logowania</strong> w liście użytkowników',
],
'improve': [
'<strong>Automatyczne odświeżanie tokenów OAuth</strong> - strona integracji nie wymaga ponownego logowania po wygaśnięciu tokenu',
'<strong>Stylizowane okno potwierdzenia rozłączenia OAuth</strong> - zamiast standardowego okna przeglądarki',
'<strong>Wydzielone przyciski audytu na profilu firmy</strong> - przeniesione do osobnego wiersza dla lepszej czytelności',
'<strong>Link "Mój pulpit" na profilu firmy</strong> - szybki powrót do panelu użytkownika',
'<strong>Dostęp do audytów ograniczony do wyznaczonego użytkownika</strong>',
'<strong>Menu uproszczone dla zwykłych użytkowników</strong> - ukrycie opcji widocznych tylko dla właściciela platformy',
],
'fix': [
'<strong>Polskie znaki w audytach</strong> - naprawiono wyświetlanie polskich znaków diakrytycznych w szablonach audytu',
'<strong>Baza wiedzy ZOPK</strong> - naprawiono model embeddingów, scraper treści i polskie komunikaty błędów',
'<strong>Pulpit użytkownika</strong> - naprawiono błąd wylogowywania przy wyświetlaniu dashboardu z powiązanymi firmami',
],
},
{
'version': 'v1.28.0',
'date': '8 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Audyt SEO: dane z Google Search Console</strong> - kliknięcia, wyświetlenia i średnia pozycja z Google',
'<strong>Audyt SEO: dane CrUX</strong> - rzeczywiste pomiary szybkości od użytkowników Chrome',
'<strong>Audyt SEO: nagłówki bezpieczeństwa</strong> - sprawdzanie konfiguracji HTTPS i nagłówków ochronnych',
'<strong>Audyt SEO: formaty obrazów</strong> - analiza wykorzystania nowoczesnych formatów WebP/AVIF',
'<strong>Audyt SEO: pasek postępu i podsumowanie wyników</strong> - czytelna wizualizacja stanu strony',
'<strong>Audyt SEO: instrukcje konfiguracji Search Console</strong> - krok po kroku dla firm bez połączonego konta',
'<strong>Audyt GBP: migracja na Places API (New)</strong> - nowsze i dokładniejsze dane o firmach w Google Maps',
'<strong>Audyt GBP: link do recenzji, wskazówki dojazdu, status otwarcia</strong>',
'<strong>Audyt AI: analiza sentymentu recenzji i benchmarki branżowe</strong>',
'<strong>Integracje OAuth</strong> - framework do łączenia kont Google i Meta z platformą',
],
'improve': [
'<strong>Migracja FID na INP</strong> - nowy standard Google do pomiaru interaktywności strony',
'<strong>Rozszerzone wyświetlanie danych GBP i Social Media</strong> w panelach audytu',
],
'fix': [
'<strong>Audyt GBP</strong> - naprawiono fałszywe wykrywanie zdjęć i logo',
'<strong>Audyt SEO</strong> - naprawiono zamianę klucza fid_ms na inp_ms w trasie',
'<strong>Search Console</strong> - obsługa formatu domain property (sc-domain:)',
],
},
{
'version': 'v1.27.0',
'date': '6 lutego 2026',
'badges': ['security', 'new', 'improve', 'fix'],
'security': [
'<strong>Przegląd bezpieczeństwa platformy</strong> - naprawiono 8 wykrytych luk (1 krytyczna, 7 średnich)',
'<strong>Ochrona wyszukiwarki i bazy wiedzy ZOPK</strong> - zabezpieczenie przed atakami przez złośliwe zapytania',
'<strong>Bezpieczne zapisywanie treści</strong> - oczyszczanie HTML w ogłoszeniach, wydarzeniach i protokołach Rady',
'<strong>Ochrona kluczy dostępowych</strong> - klucze API nie są już widoczne w logach systemowych',
'<strong>Zabezpieczenie formularzy</strong> - dodanie ochrony przed nieautoryzowanym wysyłaniem w chacie i 3 formularzach',
],
'new': [
# Forum
'<strong>Powiadomienia email z forum</strong> - otrzymujesz email gdy ktoś odpowie w temacie, w którym uczestniczysz',
'<strong>Automatyczna subskrypcja tematów</strong> - po dodaniu odpowiedzi automatycznie śledzisz dalszą dyskusję',
'<strong>Rezygnacja z powiadomień</strong> - link w każdym emailu pozwala wyłączyć powiadomienia dla danego tematu',
# NordaGPT
'<strong>NordaGPT zna Izbę NORDA</strong> - chatbot odpowiada na pytania o misję, zarząd (16 osób), Akademię NORDA i Chwilę dla Biznesu',
'<strong>Strategia 2026-2031 w NordaGPT</strong> - chatbot zna 3 kierunki rozwoju Izby, cel 30-lecia i kontekst regionalny Kaszub',
'<strong>Projekty członkowskie w NordaGPT</strong> - chatbot zna projekty Energo Velo i Żarnowiecki Ring',
# Dashboard
'<strong>Sekcja "Co nowego w Izbie?" na pulpicie</strong> - po zalogowaniu widzisz: wydarzenia, ogłoszenia, tematy forum, oferty B2B i nowe firmy',
'<strong>Aktualne dane na pulpicie</strong> - liczba nieprzeczytanych powiadomień i nadchodzących wydarzeń zamiast pustych statystyk',
# Profil firmy - edycja
'★ <strong>Edycja profilu firmy przez właściciela</strong> - właściciel może sam edytować opisy, usługi, kontakty i social media bez pomocy administratora',
'<strong>Podział uprawnień w edycji</strong> - dane formalne (NIP, KRS, nazwa) zmienia tylko administrator; dane marketingowe mogą edytować uprawnieni pracownicy',
'<strong>Więcej informacji na profilu firmy</strong> - wyświetlanie usług, technologii, zasięgu działania, języków, historii i wartości firmy',
# Rejestry urzędowe
'★ <strong>Pobieranie danych z rejestrów urzędowych</strong> - administrator jednym kliknięciem pobiera dane firmy z KRS, Białej Listy VAT lub CEIDG',
'<strong>Automatyczny dobór rejestru</strong> - system sam wybiera właściwy rejestr: KRS dla spółek, CEIDG dla jednoosobowych działalności',
'<strong>Import zarządu i branż z KRS</strong> - pobierane są osoby w zarządzie i kody PKD (branże działalności)',
# Social audit
'<strong>Ostrzeżenie o adresie Facebook</strong> - audyt wykrywa firmy używające numerycznego ID zamiast własnej nazwy na Facebooku',
'<strong>Zalecenia dla Facebooka</strong> - podpowiedź aby przekształcić profil osobisty w stronę firmową z czytelnym adresem',
'<strong>Zalecenia w panelu admina</strong> - administrator widzi kolorowe zalecenia dla każdej firmy (np. brak Facebooka, brak Instagrama)',
# Loga firm
'<strong>Nowe firmy: Termo i Studio N°33</strong> - dodano logotypy nowych firm członkowskich',
],
'improve': [
'<strong>Nowy wygląd formularza edycji firmy</strong> - czytelniejszy układ z zakładkami, ikonami i licznikiem znaków',
'<strong>Czytelniejsze podpowiedzi</strong> - po najechaniu na awatar na forum i w ogłoszeniach B2B widać czytelną etykietkę z imieniem',
'<strong>Lepsza detekcja adresów Facebook</strong> - poprawne rozpoznawanie profili z numerycznym ID i nietypowych adresów',
'<strong>Zalecenia social media nawet gdy wszystko jest OK</strong> - wyświetlanie podpowiedzi np. o zmianie adresu Facebook, nawet gdy firma ma wszystkie platformy',
'<strong>Kolumna zaleceń w panelu Social Audit</strong> - kolorowe etykiety (czerwone, pomarańczowe, szare, zielone) dla szybkiej oceny',
'<strong>Pulpit z dwukolumnowym układem</strong> - widgety z aktywnością Izby czytelnie rozmieszczone na ekranie',
],
'fix': [
'<strong>Zatwierdzanie propozycji AI</strong> - naprawiono błąd przy klikaniu "Akceptuj i dodaj do profilu"',
'<strong>Linki do Facebooka</strong> - naprawiono błędne linki dla firm z numerycznym ID na Facebooku',
'<strong>Audyt SEO ponownie dostępny</strong> - przywrócono działanie usługi audytu SEO po reorganizacji kodu',
'<strong>Audyt Google Business Profile ponownie dostępny</strong> - przywrócono działanie usługi audytu GBP',
'<strong>Wyświetlanie profilu firmy</strong> - naprawiono błąd uniemożliwiający otwarcie niektórych profili firm',
'<strong>Podpowiedzi na awatarach</strong> - poprawiona czytelność etykietek z imionami na forum i w ogłoszeniach',
],
},
{
'version': 'v1.26.0',
'date': '5 lutego 2026',
'badges': ['security', 'improve'],
'security': [
'<strong>System uprawnień: 154 trasy zabezpieczone</strong> - każda strona administracyjna wymaga teraz odpowiedniego poziomu dostępu',
'<strong>6-poziomowa hierarchia ról</strong> - od zwykłego użytkownika przez członka, pracownika, kierownika po administratora',
'<strong>Menu dostosowane do roli</strong> - kierownik biura widzi tylko te opcje, do których ma uprawnienia',
'<strong>Wybór roli przy tworzeniu użytkownika</strong> - zamiast prostego "tak/nie" administrator wybiera konkretny poziom dostępu',
],
'improve': [
'<strong>23 testy automatyczne dla systemu ról</strong> - weryfikacja poprawności uprawnień na każdym poziomie',
'<strong>Trwałe usuwanie firm</strong> - zarchiwizowane firmy mogą być trwale usunięte przez administratora (nieodwracalne)',
],
},
{
'version': 'v1.25.0',
'date': '4 lutego 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Strefa RADA</strong> - zamknięta sekcja dla członków Rady Izby z listą posiedzeń i członków',
'<strong>Zarządzanie posiedzeniami Rady</strong> - program, lista obecności i protokół w jednym miejscu',
'<strong>Edytor protokołu</strong> - zapisywanie ustaleń, decyzji i zadań z osobą odpowiedzialną i terminem',
'<strong>Pobieranie PDF</strong> - program posiedzenia i protokół do pobrania jako dokument PDF',
'<strong>Lista obecności z kworum</strong> - automatyczne liczenie obecnych i sprawdzanie kworum',
'<strong>Publikowanie programu i protokołu</strong> - osobne publikowanie każdego dokumentu',
'<strong>Korzyści dla Członków</strong> - oferty partnerskie (WisprFlow AI) dostępne dla członków Izby',
'<strong>Strona korzyści</strong> - przegląd ofert partnerskich z linkami do wersji demonstracyjnych',
'<strong>Ulepszona rejestracja</strong> - po weryfikacji email automatyczne zalogowanie i przekierowanie',
'<strong>Wydarzenia Rady</strong> widoczne tylko dla członków Izby',
'<strong>Status wniosku członkowskiego</strong> - po złożeniu wniosku widać jego aktualny stan',
'<strong>Powiadomienie dla administratora</strong> o nowym wniosku członkowskim',
'<strong>Szczegóły profilu firmy</strong> widoczne tylko dla członków Izby',
],
'improve': [
'<strong>Statusy posiedzeń jako klikalne linki</strong> do programu i protokołu',
'<strong>Czytelne wyświetlanie przebiegu posiedzenia</strong> z decyzjami i zadaniami',
'<strong>Środowisko testowe</strong> oznaczone wizualnie, aby nie pomylić z produkcją',
'Zablokowane wersje bibliotek dla stabilności platformy',
'Aktualizacja bibliotek systemowych',
'<strong>Strefa RADA uproszczona</strong> - skupiona na posiedzeniach',
'<strong>Korzyści</strong> - dane o prowizjach widoczne tylko dla właściciela oferty',
'<strong>Trwałe usuwanie firm</strong> - administrator może nieodwracalnie usunąć zarchiwizowane firmy',
],
'fix': [
'<strong>Naprawiono zabezpieczenie formularzy</strong> publikacji programu i protokołu',
'Naprawiono wyświetlanie posiedzeń bez programu lub punktów obrad',
'Naprawiono przycisk potwierdzenia udziału w wydarzeniach',
'Naprawiono link do składania wniosku członkowskiego',
'Obsługa sytuacji gdy generowanie PDF jest tymczasowo niedostępne',
'<strong>Naprawiono błąd przy usuwaniu użytkowników</strong> powiązanych z innymi danymi',
],
},
{
'version': 'v1.24.0',
'date': '2 lutego 2026',
'badges': ['new', 'improve'],
'new': [
'<strong>Środowisko testowe</strong> - osobny serwer do sprawdzania zmian przed wdrożeniem',
'<strong>Automatyczne testy</strong> - każda zmiana w kodzie jest automatycznie sprawdzana',
'<strong>Testy logowania i sesji</strong> użytkowników',
'<strong>Testy bezpieczeństwa</strong> - weryfikacja ochrony przed najczęstszymi atakami',
'<strong>Testy w przeglądarce</strong> - automatyczne sprawdzanie działania strony',
'<strong>Automatyczna weryfikacja</strong> po każdym wdrożeniu na produkcję',
],
'improve': [
'Automatyczna kontrola jakości kodu przed zapisaniem zmian',
'Narzędzia do utrzymania spójności kodu',
'Wskaźnik statusu testów widoczny na stronie projektu',
],
},
{
'version': 'v1.23.0',
'date': '1 lutego 2026',
'badges': ['security', 'new', 'improve', 'fix'],
'security': [
'<strong>6 poziomów dostępu</strong> - od gościa przez członka, pracownika, kierownika po administratora',
'<strong>NordaGPT dostępny tylko dla członków Izby</strong>',
'<strong>Wiadomości prywatne</strong> tylko dla członków Izby',
'<strong>Tablica ogłoszeń B2B</strong> tylko dla członków Izby',
'<strong>Dane kontaktowe firm</strong> widoczne tylko dla członków Izby',
],
'new': [
'★ <strong>Składanie wniosków o członkostwo</strong> - formularz, weryfikacja danych, zatwierdzanie przez admina',
'<strong>Automatyczne wyszukiwanie danych firmy</strong> po numerze NIP',
'<strong>Porównanie danych</strong> podanych przez użytkownika z danymi z rejestrów urzędowych',
'<strong>Zatwierdzanie danych z rejestrów</strong> - użytkownik może zatwierdzić lub odrzucić pobrane dane',
'<strong>Historia procesu</strong> - oś czasu wszystkich kroków od złożenia do zatwierdzenia wniosku',
'<strong>Powiadomienie dla administratora</strong> o decyzji użytkownika ws. danych z rejestrów',
'<strong>Sekcja "Dane z rejestrów urzędowych"</strong> na profilu firmy (KRS lub CEIDG)',
'<strong>Pełne dane z KRS</strong> - kapitał zakładowy, sposób reprezentacji, wspólnicy',
'<strong>Automatyczny dobór rejestru</strong> - KRS dla spółek, CEIDG dla jednoosobowych firm',
'<strong>Automatyczne pobieranie danych z KRS</strong> przy zatwierdzaniu wniosku',
'Strona promocyjna NordaGPT dla osób niebędących członkami',
'Osobne uprawnienia dla kierownika biura Izby',
'Funkcje portalu wymagające członkostwa w Izbie',
'Panel przypisywania ról użytkownikom',
'<strong>Automatyczna aktualizacja opisów firm</strong> - AI analizuje strony internetowe członków',
],
'improve': [
'<strong>Czytelniejszy profil firmy</strong> - usunięcie powtarzających się informacji',
'Dane kontaktowe zebrane w jednym miejscu na profilu',
'Podział funkcji administracyjnych według poziomu uprawnień',
'Moderacja forum dostępna dla uprawnionych osób',
'Menu dostosowane do uprawnień użytkownika',
'Usunięcie automatycznie generowanych sekcji z profilu firmy',
'Tymczasowe ukrycie sekcji rekomendacji',
],
'fix': [
'<strong>Naprawiono zapisywanie adresu</strong> przy tworzeniu nowej firmy',
'Naprawiono linki do profili firm',
'Naprawiono zabezpieczenie formularzy członkostwa',
'Naprawiono błąd przy składaniu wniosku członkowskiego',
'Naprawiono zapisywanie historii procesu członkostwa',
'Naprawiono okno potwierdzenia, które traciło dane po zamknięciu',
],
},
{
'version': 'v1.22.0',
'date': '31 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'★ <strong>Tablica B2B: Przycisk "Jestem zainteresowany"</strong> - wyrażenie zainteresowania ofertą',
'<strong>Tablica B2B: Publiczne pytania i odpowiedzi</strong> pod ogłoszeniami',
'<strong>Tablica B2B: Wysyłanie wiadomości</strong> bezpośrednio z ogłoszenia',
'Tablica B2B: Autor widzi kto jest zainteresowany jego ofertą',
'Tablica B2B: Oznaczenie wiadomości powiązanych z ogłoszeniem',
'<strong>Forum: Informacja kto przeczytał</strong> każdą odpowiedź',
'<strong>Tablica B2B: Informacja kto widział</strong> ogłoszenie',
'<strong>Panel admina: Zarządzanie firmami</strong> - lista, edycja, statystyki',
'<strong>Panel admina: Zarządzanie osobami</strong> - dane z KRS i powiązania z firmami',
'<strong>Panel admina: Przegląd stanu platformy</strong> - certyfikaty, bezpieczeństwo',
'<strong>Rejestr logowań</strong> - kto i kiedy się logował do platformy',
'<strong>Forum: Reakcje emoji</strong> na wpisy i odpowiedzi',
'<strong>Forum: Śledzenie tematów</strong> z powiadomieniami o nowych odpowiedziach',
'<strong>Forum: Edycja własnych wpisów</strong> (do 24 godzin)',
'<strong>Forum: Zgłaszanie nieodpowiednich treści</strong>',
'<strong>Forum: Oznaczanie najlepszej odpowiedzi</strong> jako rozwiązanie',
'Forum: Statystyki aktywności użytkownika',
'Forum: Formatowanie tekstu (pogrubienie, listy, linki)',
'Forum: Oznaczanie @użytkowników z powiadomieniami',
'<strong>Panel admina: Analityka forum</strong> - wykresy aktywności i ranking użytkowników',
'Panel admina: Eksport aktywności forum do arkusza',
'Panel admina: Zbiorcze zarządzanie tematami forum',
'Panel admina: Przenoszenie tematów między kategoriami',
'Panel admina: Łączenie powiązanych tematów forum',
'Panel admina: Wyszukiwarka z dostępem do usuniętych treści',
'Panel admina: Historia aktywności użytkowników na forum',
'Panel admina: Przywracanie usuniętych wpisów na forum',
'Menu admina: Szybki dostęp do Forum, Ogłoszeń i Analityki AI',
],
'improve': [
'<strong>Reorganizacja kodu platformy</strong> dla łatwiejszego rozwoju',
'Forum: Oznaczenie "(Ty)" przy własnym awatarze',
'Czytelniejszy układ informacji o certyfikatach bezpieczeństwa',
'Porządki w kodzie platformy',
],
'fix': [
'<strong>NordaGPT: Naprawiono pole wpisywania wiadomości</strong>, które było ucięte',
],
},
{
'version': 'v1.21.0',
'date': '30 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Moje konto</strong> - edycja danych osobowych, ustawienia prywatności i bezpieczeństwa',
'<strong>Moderacja forum</strong> - administrator może usuwać, przypinać i blokować wpisy',
'<strong>Moderacja ogłoszeń B2B</strong> - administrator może usuwać i dezaktywować ogłoszenia',
'Podgląd hasła - ikonka oka pozwala zobaczyć wpisywane hasło',
'Ładniejsze okna potwierdzenia na forum',
'Ładniejsze okna potwierdzenia w ogłoszeniach B2B',
'Wątek na forum do zgłaszania pomysłów i uwag',
],
'improve': [
'Poprawna nazwa platformy na stronie rejestracji',
'Przyjazna strona informacyjna podczas aktualizacji platformy',
],
'fix': [
'<strong>Reset hasła</strong> nie wymaga już ponownej weryfikacji adresu email',
'Usunięto tymczasowe wideo z sekcji edukacyjnej',
],
},
{
'version': 'v1.20.0',
'date': '29 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>NordaGPT: Nowy silnik AI</strong> - Google Gemini 3 Flash z lepszym rozumieniem pytań',
'<strong>NordaGPT: Dwa tryby</strong> - podstawowy (bezpłatny) i zaawansowany (dokładniejszy)',
'NordaGPT: 7x lepsze rozumowanie i dokładniejsze odpowiedzi',
'NordaGPT: Informacja o szacowanym koszcie użytkowania',
'★ <strong>Aplikacja mobilna</strong> - portal można zainstalować na telefonie jak aplikację (iOS/Android)',
'Aktualności: Ogłoszenie może należeć do kilku kategorii jednocześnie',
'Aktualności: Nowe kategorie - Wewnętrzne, Zewnętrzne, Wydarzenie, Okazja biznesowa, Partnerstwo',
'Sekcja edukacyjna: Materiały wideo do obejrzenia na portalu',
'Film powitalny "Wprowadzenie do Norda Biznes Partner"',
'<strong>Administrator otrzymuje email</strong> o każdej nowej rejestracji',
],
'improve': [
'Nowa ikona NordaGPT na stronie głównej',
'Porządki w stopce strony',
],
'fix': [
'Naprawiono błąd przy dodawaniu ogłoszeń B2B',
'Naprawiono błąd przy dodawaniu wydarzeń do kalendarza',
'Naprawiono nawigację w module kontaktów',
],
},
{
'version': 'v1.19.0',
'date': '28 stycznia 2026',
'badges': ['new', 'improve', 'security'],
'new': [
'<strong>Ukrywanie telefonu i email</strong> - można wybrać w ustawieniach, co jest widoczne na profilu',
'<strong>Blokowanie użytkowników</strong> - zablokowana osoba nie może wysyłać wiadomości',
'Wybór preferowanego sposobu kontaktu (email, telefon, portal)',
'<strong>Kategorie branżowe</strong> - 4 główne grupy z podkategoriami',
'Oznaczenie firm z niekompletnym profilem do uzupełnienia',
'Nowe podkategorie branżowe: Budownictwo, Produkcja, Usługi finansowe',
'<strong>Nowa sekcja Edukacja</strong> w menu platformy',
'Panel zbierania opinii i pomysłów od użytkowników',
'Rozszerzony monitoring stanu platformy',
],
'improve': [
'Katalog: Wyraźne oznaczenie wybranej kategorii',
'Kategorie posortowane od największej liczby firm',
],
'security': [
'<strong>Ochrona danych osobowych</strong> - chatbot automatycznie ukrywa numery PESEL, karty i IBAN',
'<strong>Prywatność rozmów</strong> - każdy użytkownik widzi tylko swoje rozmowy z chatbotem',
'Anonimowe statystyki rozmów z chatbotem w panelu admina',
],
},
{
'version': 'v1.17.0',
'date': '26 stycznia 2026',
'badges': ['new'],
'new': [
'<strong>Sekcja Aktualności</strong> - wiadomości i ogłoszenia dla członków Izby',
'Panel zarządzania aktualnościami dla administratora',
'Kategorie aktualności, możliwość przypinania ważnych ogłoszeń',
'Załączniki PDF i linki w aktualnościach',
'Pierwsze ogłoszenia: Baza noclegowa ARP, Konkurs Tytani Przedsiębiorczości',
],
},
{
'version': 'v1.16.0',
'date': '14 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Ochrona geograficzna</strong> - blokowanie dostępu z krajów wysokiego ryzyka',
'<strong>Własna domena email</strong> - wiadomości wysyłane z adresu @nordabiznes.pl',
'<strong>Raporty</strong> - staż członkostwa, obecność w social media, struktura branżowa',
'Data przystąpienia do Izby na profilu firmy z informacją o stażu',
'Pobieranie danych jednoosobowych firm z rejestru CEIDG',
'Panel bezpieczeństwa z oceną poziomu ochrony platformy',
],
'improve': [
'Uzupełniono rok założenia dla 71 firm (64% katalogowanych)',
'Uzupełniono daty przystąpienia do Izby dla 57 firm (od 1997 roku)',
],
'fix': [
'Naprawiono wyświetlanie polskich znaków w statystykach',
],
},
{
'version': 'v1.15.0',
'date': '13 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>NordaGPT zna więcej danych</strong> - rekomendacje, kalendarz, ogłoszenia B2B, forum i dane KRS',
'<strong>NordaGPT: Klikalne linki</strong> i adresy email w odpowiedziach chatbota',
'<strong>NordaGPT: Szybki dostęp</strong> do chatbota ze strony głównej',
'<strong>Kalendarz: Widok miesięczny</strong> z szybkim potwierdzaniem udziału',
'Najbliższe wydarzenie widoczne na stronie głównej z listą uczestników',
'<strong>Wzbogacanie profili firm przez AI</strong> - automatyczne uzupełnianie informacji z internetu',
'<strong>Sprawdzanie danych KRS</strong> z raportami postępu',
'<strong>Panel analityki</strong> - statystyki odwiedzin i aktywności użytkowników',
'Profil firmy: Pełna lista branż (kody PKD) i dane właściciela',
'Zielone oznaczenie przy osobach zweryfikowanych w rejestrze KRS',
],
'improve': [
'Czytelniejsze formatowanie odpowiedzi NordaGPT',
'Możliwość zwinięcia bannera NordaGPT na stronie głównej',
],
'fix': [
'Zwiększony limit logowań i audytów SEO',
],
},
{
'version': 'v1.14.0',
'date': '12 stycznia 2026',
'badges': ['new', 'improve', 'fix'],
'new': [
'<strong>Audyt wizytówki Google</strong> - sprawdzanie kompletności profilu Google dla każdej firmy',
'Poradnik "Jak działa wizytówka Google?" w sekcji audytu',
'Wyniki audytów widoczne bezpośrednio na profilu firmy',
],
'improve': [
'Jednolita 5-stopniowa skala ocen we wszystkich audytach',
'Wynik audytu social media jako procent zamiast liczby platform',
],
'fix': [
'Kategorie Google wyświetlane po polsku',
],
},
{
'version': 'v1.13.0',
'date': '11 stycznia 2026',
'badges': ['new', 'improve'],
'new': [
'★ <strong>Mapa Powiązań</strong> - interaktywna wizualizacja powiązań między firmami i osobami',
'<strong>Profile osób</strong> - dane z rejestrów urzędowych i portalu',
'<strong>NordaGPT uczy się z opinii użytkowników</strong> i poprawia odpowiedzi',
'Wyszukiwanie osób po częściowym imieniu lub nazwisku',
'Logo firm widoczne w wynikach wyszukiwania',
'Panel użycia AI - statystyki rozmów dla każdego użytkownika',
],
'improve': [
'Mapa Powiązań: pełnoekranowy widok z podpowiedziami po najechaniu',
'Ładniejsze powiadomienia zamiast systemowych okien',
],
},
{
'version': 'v1.11.0',
'date': '10 stycznia 2026',
'badges': ['new', 'improve', 'security'],
'new': [
'<strong>Forum: Wstawianie zdjęć</strong> - przeciągnij, wklej ze schowka, do 10 plików',
'<strong>Forum: Kategorie wpisów</strong> - Propozycja, Błąd, Pytanie',
'<strong>Kompletna dokumentacja techniczna</strong> platformy',
],
'improve': [
'Bezpieczne przesyłanie plików ze sprawdzaniem zawartości',
],
'security': [
'<strong>Usunięcie haseł z kodu źródłowego</strong>',
'Zmiana hasła bazy danych na serwerze produkcyjnym',
],
},
{
'version': 'v1.9.0',
'date': '9 stycznia 2026',
'badges': ['new', 'improve'],
'new': [
'<strong>Audyt wizytówek Google</strong> - przegląd profili Google Business wszystkich firm',
'<strong>Audyt Social Media</strong> - sprawdzanie obecności firm w mediach społecznościowych',
'<strong>Tworzenie użytkowników przez AI</strong> - wystarczy wkleić tekst lub zrzut ekranu',
],
'improve': [
'Nowy pasek administracyjny z pogrupowanymi funkcjami',
],
},
{
'version': 'v1.8.0',
'date': '8 stycznia 2026',
'badges': ['new'],
'new': [
'<strong>Audyt IT</strong> - sprawdzanie infrastruktury informatycznej firm członkowskich',
'Eksport wyników audytu IT do arkusza kalkulacyjnego',
],
},
{
'version': 'v1.7.0',
'date': '6 stycznia 2026',
'badges': ['new'],
'new': [
'<strong>Audyt SEO</strong> - analiza widoczności stron internetowych firm w wyszukiwarkach',
'<strong>Ocena szybkości stron</strong> przez Google PageSpeed',
],
},
{
'version': 'v1.6.0',
'date': '29 grudnia 2025',
'badges': ['new'],
'new': [
'<strong>Wzmianki medialne</strong> - automatyczne wyszukiwanie artykułów o firmach członkowskich',
'Panel moderacji wzmianek dla administratora',
'Wyszukiwanie wzmianek przez wyszukiwarkę Brave',
],
},
{
'version': 'v1.5.0',
'date': '15 grudnia 2025',
'badges': ['new', 'improve'],
'new': [
'<strong>Panel Social Media</strong> - zarządzanie profilami firm w mediach społecznościowych',
'Sprawdzanie czy profile social media firm są aktywne',
],
'improve': [
'Sekcja Social Media na profilu firmy',
],
},
{
'version': 'v1.4.0',
'date': '1 grudnia 2025',
'badges': ['new'],
'new': [
'<strong>Rekomendacje</strong> - firmy mogą polecać się nawzajem',
'<strong>Panel składek członkowskich</strong>',
'<strong>Kalendarz wydarzeń</strong> Izby',
],
},
{
'version': 'v1.3.0',
'date': '28 listopada 2025',
'badges': ['new', 'improve'],
'new': [
'★ <strong>Chatbot NordaGPT</strong> - asystent AI znający wszystkie firmy członkowskie',
'<strong>Wyszukiwarka firm</strong> - rozpoznaje synonimy i literówki',
],
'improve': [
'Szybsza i dokładniejsza wyszukiwarka',
],
},
{
'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': [
'Strona dostosowana do telefonów i tabletów',
],
},
{
'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, branży i usługach',
'Profile firm z pełnymi danymi kontaktowymi',
],
},
]
@bp.route('/release-notes')
def release_notes():
"""Historia zmian platformy."""
releases = _get_releases()
# 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)
@bp.route('/polityka-prywatnosci')
def polityka_prywatnosci():
"""Polityka prywatności platformy."""
return render_template('polityka_prywatnosci.html')
@bp.route('/regulamin')
def regulamin():
"""Regulamin platformy."""
return render_template('regulamin.html')
@bp.route('/zainstaluj-aplikacje')
def pwa_install():
"""Instrukcja instalacji PWA na telefonie."""
return render_template('pwa_install.html')
@bp.route('/sw.js')
def service_worker():
"""Service worker served from root scope for PWA installability."""
return current_app.send_static_file('sw.js'), 200, {
'Content-Type': 'application/javascript',
'Service-Worker-Allowed': '/'
}
@bp.route('/robots.txt')
def robots_txt():
"""Robots.txt for search engine crawlers."""
content = """User-agent: *
Allow: /
Disallow: /admin/
Disallow: /api/
Disallow: /chat
Disallow: /login
Disallow: /register
Disallow: /dashboard
Sitemap: https://nordabiznes.pl/sitemap.xml
"""
return Response(content, mimetype='text/plain')
@bp.route('/sitemap.xml')
def sitemap_xml():
"""Sitemap XML for search engines."""
today = date.today().isoformat()
xml = f"""<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://nordabiznes.pl/</loc>
<lastmod>{today}</lastmod>
<changefreq>daily</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://nordabiznes.pl/release-notes</loc>
<lastmod>{today}</lastmod>
<changefreq>weekly</changefreq>
<priority>0.5</priority>
</url>
<url>
<loc>https://nordabiznes.pl/zainstaluj-aplikacje</loc>
<lastmod>{today}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.4</priority>
</url>
<url>
<loc>https://nordabiznes.pl/polityka-prywatnosci</loc>
<lastmod>{today}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
<url>
<loc>https://nordabiznes.pl/regulamin</loc>
<lastmod>{today}</lastmod>
<changefreq>monthly</changefreq>
<priority>0.3</priority>
</url>
</urlset>"""
return Response(xml, mimetype='application/xml')