""" 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, OAuthToken ) 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/') @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, # GSC Extended data 'gsc_top_pages': getattr(analysis, 'gsc_top_pages', None), 'gsc_device_breakdown': getattr(analysis, 'gsc_device_breakdown', None), 'gsc_index_status': getattr(analysis, 'gsc_index_status', None), 'gsc_last_crawl': getattr(analysis, 'gsc_last_crawl', None), 'gsc_crawled_as': getattr(analysis, 'gsc_crawled_as', None), 'gsc_sitemaps': getattr(analysis, 'gsc_sitemaps', None), 'gsc_country_breakdown': getattr(analysis, 'gsc_country_breakdown', None), 'gsc_search_type_breakdown': getattr(analysis, 'gsc_search_type_breakdown', None), 'gsc_trend_data': getattr(analysis, 'gsc_trend_data', None), # 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], } # Check if company has GSC OAuth token has_gsc_token = db.query(OAuthToken).filter( OAuthToken.company_id == company.id, OAuthToken.provider == 'google', OAuthToken.service == 'search_console' ).first() is not None # 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, has_gsc_token=has_gsc_token ) finally: db.close() # ============================================================ # SOCIAL MEDIA AUDIT USER-FACING DASHBOARD # ============================================================ @bp.route('/social/') @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/') @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, # GBP Performance API data 'gbp_impressions_maps': getattr(analysis, 'gbp_impressions_maps', None), 'gbp_impressions_search': getattr(analysis, 'gbp_impressions_search', None), 'gbp_call_clicks': getattr(analysis, 'gbp_call_clicks', None), 'gbp_website_clicks': getattr(analysis, 'gbp_website_clicks', None), 'gbp_direction_requests': getattr(analysis, 'gbp_direction_requests', None), 'gbp_conversations': getattr(analysis, 'gbp_conversations', None), 'gbp_search_keywords': getattr(analysis, 'gbp_search_keywords', None), 'gbp_performance_period_days': getattr(analysis, 'gbp_performance_period_days', None), # Owner data 'google_owner_responses_count': getattr(analysis, 'google_owner_responses_count', None), 'google_review_response_rate': float(analysis.google_review_response_rate) if getattr(analysis, 'google_review_response_rate', None) is not None else None, 'google_posts_data': getattr(analysis, 'google_posts_data', None), 'google_posts_count': getattr(analysis, 'google_posts_count', None), } # 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/') @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()