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
Add GSC columns to DB, persist OAuth data during audits, and render clicks/impressions/CTR/position with top queries table on the dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
494 lines
20 KiB
Python
494 lines
20 KiB
Python
"""
|
|
Audit Dashboard Routes - Audit blueprint
|
|
|
|
Migrated from app.py as part of the blueprint refactoring.
|
|
Contains user-facing audit dashboard pages for SEO, GBP, Social Media, and IT.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from flask import flash, redirect, render_template, url_for
|
|
from flask_login import current_user, login_required
|
|
|
|
from database import (
|
|
SessionLocal, Company, CompanyWebsiteAnalysis,
|
|
CompanySocialMedia, ITAudit, CompanyCitation, GBPReview
|
|
)
|
|
from . import bp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# Check if GBP audit service is available
|
|
try:
|
|
from gbp_audit_service import (
|
|
get_company_audit as gbp_get_company_audit
|
|
)
|
|
GBP_AUDIT_AVAILABLE = True
|
|
GBP_AUDIT_VERSION = '1.0'
|
|
except ImportError:
|
|
GBP_AUDIT_AVAILABLE = False
|
|
GBP_AUDIT_VERSION = None
|
|
gbp_get_company_audit = None
|
|
|
|
|
|
# ============================================================
|
|
# SEO AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/seo/<slug>')
|
|
@login_required
|
|
def seo_audit_dashboard(slug):
|
|
"""
|
|
User-facing SEO audit dashboard for a specific company.
|
|
|
|
Displays SEO audit results with:
|
|
- PageSpeed Insights scores (SEO, Performance, Accessibility, Best Practices)
|
|
- Website analysis data
|
|
- Improvement recommendations
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered seo_audit.html template with company and audit data
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest SEO analysis for this company
|
|
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
|
CompanyWebsiteAnalysis.company_id == company.id
|
|
).order_by(CompanyWebsiteAnalysis.seo_audited_at.desc()).first()
|
|
|
|
# Get citations for this company
|
|
citations = db.query(CompanyCitation).filter(
|
|
CompanyCitation.company_id == company.id
|
|
).order_by(CompanyCitation.directory_name).all() if analysis else []
|
|
|
|
# Build SEO data dict if analysis exists
|
|
seo_data = None
|
|
if analysis and analysis.seo_audited_at:
|
|
seo_data = {
|
|
'seo_score': analysis.pagespeed_seo_score,
|
|
'performance_score': analysis.pagespeed_performance_score,
|
|
'accessibility_score': analysis.pagespeed_accessibility_score,
|
|
'best_practices_score': analysis.pagespeed_best_practices_score,
|
|
'audited_at': analysis.seo_audited_at,
|
|
'audit_version': analysis.seo_audit_version,
|
|
'url': analysis.website_url,
|
|
# Local SEO fields
|
|
'local_seo_score': analysis.local_seo_score,
|
|
'has_local_business_schema': analysis.has_local_business_schema,
|
|
'local_business_schema_fields': analysis.local_business_schema_fields,
|
|
'nap_on_website': analysis.nap_on_website,
|
|
'has_google_maps_embed': analysis.has_google_maps_embed,
|
|
'has_local_keywords': analysis.has_local_keywords,
|
|
'local_keywords_found': analysis.local_keywords_found,
|
|
'citations_count': analysis.citations_count,
|
|
'content_freshness_score': analysis.content_freshness_score,
|
|
'last_modified_date': analysis.last_modified_at,
|
|
# Core Web Vitals
|
|
'lcp_ms': analysis.largest_contentful_paint_ms,
|
|
'inp_ms': analysis.interaction_to_next_paint_ms,
|
|
'cls': float(analysis.cumulative_layout_shift) if analysis.cumulative_layout_shift is not None else None,
|
|
# Heading structure
|
|
'h1_count': analysis.h1_count,
|
|
'h1_text': analysis.h1_text,
|
|
'h2_count': analysis.h2_count,
|
|
'h3_count': analysis.h3_count,
|
|
# Image SEO
|
|
'total_images': analysis.total_images,
|
|
'images_without_alt': analysis.images_without_alt,
|
|
# Links
|
|
'internal_links_count': analysis.internal_links_count,
|
|
'external_links_count': analysis.external_links_count,
|
|
'broken_links_count': analysis.broken_links_count,
|
|
# SSL
|
|
'has_ssl': analysis.has_ssl,
|
|
'ssl_expires_at': analysis.ssl_expires_at,
|
|
# Analytics
|
|
'has_google_analytics': analysis.has_google_analytics,
|
|
'has_google_tag_manager': analysis.has_google_tag_manager,
|
|
# Social sharing
|
|
'has_og_tags': analysis.has_og_tags,
|
|
'has_twitter_cards': analysis.has_twitter_cards,
|
|
# Technical SEO details
|
|
'has_sitemap': analysis.has_sitemap,
|
|
'has_robots_txt': analysis.has_robots_txt,
|
|
'has_canonical': analysis.has_canonical,
|
|
'canonical_url': analysis.canonical_url,
|
|
'is_indexable': analysis.is_indexable,
|
|
'noindex_reason': analysis.noindex_reason,
|
|
'is_mobile_friendly': analysis.is_mobile_friendly,
|
|
'viewport_configured': analysis.viewport_configured,
|
|
'html_lang': analysis.html_lang,
|
|
'has_hreflang': analysis.has_hreflang,
|
|
# Meta tags
|
|
'meta_title': analysis.meta_title,
|
|
'meta_description': analysis.meta_description,
|
|
# Structured data
|
|
'has_structured_data': analysis.has_structured_data,
|
|
'structured_data_types': analysis.structured_data_types,
|
|
# Performance
|
|
'load_time_ms': analysis.load_time_ms,
|
|
'word_count_homepage': analysis.word_count_homepage,
|
|
# CrUX field data (real user metrics)
|
|
'crux_lcp_ms': analysis.crux_lcp_ms,
|
|
'crux_inp_ms': analysis.crux_inp_ms,
|
|
'crux_cls': float(analysis.crux_cls) if analysis.crux_cls is not None else None,
|
|
'crux_fcp_ms': analysis.crux_fcp_ms,
|
|
'crux_ttfb_ms': analysis.crux_ttfb_ms,
|
|
'crux_lcp_good_pct': float(analysis.crux_lcp_good_pct) if analysis.crux_lcp_good_pct is not None else None,
|
|
'crux_inp_good_pct': float(analysis.crux_inp_good_pct) if analysis.crux_inp_good_pct is not None else None,
|
|
# Security headers
|
|
'has_hsts': analysis.has_hsts,
|
|
'has_csp': analysis.has_csp,
|
|
'has_x_frame_options': analysis.has_x_frame_options,
|
|
'has_x_content_type_options': analysis.has_x_content_type_options,
|
|
'security_headers_count': analysis.security_headers_count,
|
|
# Image formats
|
|
'modern_image_count': analysis.modern_image_count,
|
|
'legacy_image_count': analysis.legacy_image_count,
|
|
'modern_image_ratio': float(analysis.modern_image_ratio) if analysis.modern_image_ratio is not None else None,
|
|
# Google Search Console (OAuth)
|
|
'gsc_clicks': analysis.gsc_clicks,
|
|
'gsc_impressions': analysis.gsc_impressions,
|
|
'gsc_ctr': float(analysis.gsc_ctr) if analysis.gsc_ctr is not None else None,
|
|
'gsc_avg_position': float(analysis.gsc_avg_position) if analysis.gsc_avg_position is not None else None,
|
|
'gsc_top_queries': analysis.gsc_top_queries,
|
|
'gsc_period_days': analysis.gsc_period_days,
|
|
# Citations list
|
|
'citations': [{'directory_name': c.directory_name, 'listing_url': c.listing_url, 'status': c.status, 'nap_accurate': c.nap_accurate} for c in citations],
|
|
}
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"SEO audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('seo_audit.html',
|
|
company=company,
|
|
seo_data=seo_data,
|
|
can_audit=can_audit
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# SOCIAL MEDIA AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/social/<slug>')
|
|
@login_required
|
|
def social_audit_dashboard(slug):
|
|
"""
|
|
User-facing Social Media audit dashboard for a specific company.
|
|
|
|
Displays social media presence audit with:
|
|
- Overall presence score (platforms found / total platforms)
|
|
- Platform-by-platform status
|
|
- Profile validation status
|
|
- Recommendations for missing platforms
|
|
|
|
Access control:
|
|
- Admins: Can view all companies
|
|
- Regular users: Can only view their own company
|
|
|
|
Args:
|
|
slug: Company URL slug
|
|
|
|
Returns:
|
|
Rendered social_audit.html template with company and social data
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control - users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień do wyświetlenia audytu social media tej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get social media profiles for this company
|
|
social_profiles = db.query(CompanySocialMedia).filter(
|
|
CompanySocialMedia.company_id == company.id
|
|
).all()
|
|
|
|
# Define all platforms we track
|
|
all_platforms = ['facebook', 'instagram', 'linkedin', 'youtube', 'twitter', 'tiktok']
|
|
|
|
# Build social media data
|
|
profiles_dict = {}
|
|
for profile in social_profiles:
|
|
profiles_dict[profile.platform] = {
|
|
'url': profile.url,
|
|
'is_valid': profile.is_valid,
|
|
'check_status': profile.check_status,
|
|
'page_name': profile.page_name,
|
|
'followers_count': profile.followers_count,
|
|
'verified_at': profile.verified_at,
|
|
'last_checked_at': profile.last_checked_at,
|
|
# Enhanced audit fields
|
|
'has_profile_photo': profile.has_profile_photo,
|
|
'has_cover_photo': profile.has_cover_photo,
|
|
'has_bio': profile.has_bio,
|
|
'profile_description': profile.profile_description,
|
|
'posts_count_30d': profile.posts_count_30d,
|
|
'posts_count_365d': profile.posts_count_365d,
|
|
'last_post_date': profile.last_post_date,
|
|
'posting_frequency_score': profile.posting_frequency_score,
|
|
'engagement_rate': profile.engagement_rate,
|
|
'content_types': profile.content_types,
|
|
'profile_completeness_score': profile.profile_completeness_score,
|
|
'followers_history': profile.followers_history,
|
|
'source': profile.source,
|
|
}
|
|
|
|
# Calculate score (platforms with profiles / total platforms)
|
|
platforms_with_profiles = len([p for p in all_platforms if p in profiles_dict])
|
|
total_platforms = len(all_platforms)
|
|
score = int((platforms_with_profiles / total_platforms) * 100) if total_platforms > 0 else 0
|
|
|
|
social_data = {
|
|
'profiles': profiles_dict,
|
|
'all_platforms': all_platforms,
|
|
'platforms_count': platforms_with_profiles,
|
|
'total_platforms': total_platforms,
|
|
'score': score
|
|
}
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"Social Media audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('social_audit.html',
|
|
company=company,
|
|
social_data=social_data,
|
|
can_audit=can_audit
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# GBP AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/gbp/<slug>')
|
|
@login_required
|
|
def gbp_audit_dashboard(slug):
|
|
"""
|
|
User-facing GBP audit dashboard for a specific company.
|
|
|
|
Displays Google Business Profile completeness audit results with:
|
|
- Overall completeness score (0-100)
|
|
- Field-by-field status breakdown
|
|
- AI-generated improvement recommendations
|
|
- Historical audit data
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered gbp_audit.html template with company and audit data
|
|
"""
|
|
if not GBP_AUDIT_AVAILABLE:
|
|
flash('Usługa audytu Google Business Profile jest tymczasowo niedostępna.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest audit for this company
|
|
audit = gbp_get_company_audit(db, company.id)
|
|
|
|
# Get recent reviews for this company
|
|
recent_reviews = db.query(GBPReview).filter(
|
|
GBPReview.company_id == company.id
|
|
).order_by(GBPReview.publish_time.desc()).limit(5).all() if audit else []
|
|
|
|
# Get Places API enrichment data from CompanyWebsiteAnalysis
|
|
places_data = {}
|
|
analysis = db.query(CompanyWebsiteAnalysis).filter(
|
|
CompanyWebsiteAnalysis.company_id == company.id
|
|
).order_by(CompanyWebsiteAnalysis.analyzed_at.desc()).first()
|
|
if analysis:
|
|
places_data = {
|
|
'primary_type': getattr(analysis, 'google_primary_type', None),
|
|
'editorial_summary': getattr(analysis, 'google_editorial_summary', None),
|
|
'price_level': getattr(analysis, 'google_price_level', None),
|
|
'maps_links': getattr(analysis, 'google_maps_links', None),
|
|
'open_now': getattr(analysis, 'google_open_now', None),
|
|
'google_name': analysis.google_name,
|
|
'google_address': analysis.google_address,
|
|
'google_phone': analysis.google_phone,
|
|
}
|
|
|
|
# If no audit exists, we still render the page (template handles this)
|
|
# The user can trigger an audit from the dashboard
|
|
|
|
# Determine if user can run audit (user with company edit rights)
|
|
can_audit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"GBP audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('gbp_audit.html',
|
|
company=company,
|
|
audit=audit,
|
|
recent_reviews=recent_reviews,
|
|
places_data=places_data,
|
|
can_audit=can_audit,
|
|
gbp_audit_available=GBP_AUDIT_AVAILABLE,
|
|
gbp_audit_version=GBP_AUDIT_VERSION
|
|
)
|
|
|
|
finally:
|
|
db.close()
|
|
|
|
|
|
# ============================================================
|
|
# IT AUDIT USER-FACING DASHBOARD
|
|
# ============================================================
|
|
|
|
@bp.route('/it/<slug>')
|
|
@login_required
|
|
def it_audit_dashboard(slug):
|
|
"""
|
|
User-facing IT infrastructure audit dashboard for a specific company.
|
|
|
|
Displays IT audit results with:
|
|
- Overall score and maturity level
|
|
- Security, collaboration, and completeness sub-scores
|
|
- Technology stack summary (Azure AD, M365, backup, monitoring)
|
|
- AI-generated recommendations
|
|
|
|
Access control:
|
|
- Admin users can view audit for any company
|
|
- Regular users can only view audit for their own company
|
|
|
|
Args:
|
|
slug: Company slug identifier
|
|
|
|
Returns:
|
|
Rendered it_audit.html template with company and audit data
|
|
"""
|
|
db = SessionLocal()
|
|
try:
|
|
# Find company by slug
|
|
company = db.query(Company).filter_by(slug=slug, status='active').first()
|
|
|
|
if not company:
|
|
flash('Firma nie została znaleziona.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Access control: users with company edit rights can view
|
|
if not current_user.can_edit_company(company.id):
|
|
flash('Brak uprawnień. Możesz przeglądać audyt tylko własnej firmy.', 'error')
|
|
return redirect(url_for('dashboard'))
|
|
|
|
# Get latest IT audit for this company
|
|
audit = db.query(ITAudit).filter(
|
|
ITAudit.company_id == company.id
|
|
).order_by(ITAudit.audit_date.desc()).first()
|
|
|
|
# Build audit data dict if audit exists
|
|
audit_data = None
|
|
if audit:
|
|
# Get maturity label
|
|
maturity_labels = {
|
|
'basic': 'Podstawowy',
|
|
'developing': 'Rozwijający się',
|
|
'established': 'Ugruntowany',
|
|
'advanced': 'Zaawansowany'
|
|
}
|
|
|
|
audit_data = {
|
|
'id': audit.id,
|
|
'overall_score': audit.overall_score,
|
|
'security_score': audit.security_score,
|
|
'collaboration_score': audit.collaboration_score,
|
|
'completeness_score': audit.completeness_score,
|
|
'maturity_level': audit.maturity_level,
|
|
'maturity_label': maturity_labels.get(audit.maturity_level, 'Nieznany'),
|
|
'audit_date': audit.audit_date,
|
|
'audit_source': audit.audit_source,
|
|
# Technology flags
|
|
'has_azure_ad': audit.has_azure_ad,
|
|
'has_m365': audit.has_m365,
|
|
'has_google_workspace': audit.has_google_workspace,
|
|
'has_local_ad': audit.has_local_ad,
|
|
'has_edr': audit.has_edr,
|
|
'has_mfa': audit.has_mfa,
|
|
'has_vpn': audit.has_vpn,
|
|
'has_proxmox_pbs': audit.has_proxmox_pbs,
|
|
'has_dr_plan': audit.has_dr_plan,
|
|
'has_mdm': audit.has_mdm,
|
|
# Solutions
|
|
'antivirus_solution': audit.antivirus_solution,
|
|
'backup_solution': audit.backup_solution,
|
|
'monitoring_solution': audit.monitoring_solution,
|
|
'virtualization_platform': audit.virtualization_platform,
|
|
# Collaboration flags
|
|
'open_to_shared_licensing': audit.open_to_shared_licensing,
|
|
'open_to_backup_replication': audit.open_to_backup_replication,
|
|
'open_to_teams_federation': audit.open_to_teams_federation,
|
|
'open_to_shared_monitoring': audit.open_to_shared_monitoring,
|
|
'open_to_collective_purchasing': audit.open_to_collective_purchasing,
|
|
'open_to_knowledge_sharing': audit.open_to_knowledge_sharing,
|
|
# Recommendations
|
|
'recommendations': audit.recommendations
|
|
}
|
|
|
|
# Determine if user can edit audit (user with company edit rights)
|
|
can_edit = current_user.can_edit_company(company.id)
|
|
|
|
logger.info(f"IT audit dashboard viewed by {current_user.email} for company: {company.name}")
|
|
|
|
return render_template('it_audit.html',
|
|
company=company,
|
|
audit_data=audit_data,
|
|
can_edit=can_edit
|
|
)
|
|
|
|
finally:
|
|
db.close()
|