feat: add detailed SEO audit view per company with recommendations
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

- New /admin/seo/<id> detail page with full audit breakdown
- 4 score cards with visual rings and Polish descriptions
- Recommendations section: critical/warning/info issues with actionable text
- SEO checklist: meta tags, sitemap, robots, structured data, OG tags
- Technical details: SSL, mobile, CWV metrics, security headers
- Google Business Profile section when available
- Add "Szczegoly" button in company table (replaces profile link)
- Add visible column descriptions above table (not hidden tooltips)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-11 04:54:58 +01:00
parent bc7b206741
commit b44722fd2c
3 changed files with 674 additions and 6 deletions

View File

@ -139,6 +139,180 @@ def admin_seo():
db.close()
@bp.route('/seo/<int:company_id>')
@login_required
@role_required(SystemRole.OFFICE_MANAGER)
def admin_seo_detail(company_id):
"""Detailed SEO audit view for a single company."""
if not is_audit_owner():
abort(404)
db = SessionLocal()
try:
from sqlalchemy import func
company = db.query(Company).filter_by(id=company_id).first()
if not company:
flash('Firma nie istnieje.', 'error')
return redirect(url_for('admin.admin_seo'))
analysis = db.query(CompanyWebsiteAnalysis).filter_by(
company_id=company_id
).first()
# Build recommendations from available data
recommendations = []
if not analysis:
recommendations.append({
'severity': 'info',
'text': 'Brak danych audytu. Uruchom audyt SEO dla tej firmy.'
})
else:
# SEO score
if analysis.pagespeed_seo_score is not None:
if analysis.pagespeed_seo_score < 50:
recommendations.append({
'severity': 'critical',
'text': 'Wynik SEO jest bardzo niski. Strona ma powazne problemy z optymalizacja pod wyszukiwarki.'
})
elif analysis.pagespeed_seo_score < 90:
recommendations.append({
'severity': 'warning',
'text': 'Wynik SEO jest sredni. Sa elementy do poprawy w optymalizacji pod wyszukiwarki.'
})
# Performance
if analysis.pagespeed_performance_score is not None:
if analysis.pagespeed_performance_score < 50:
recommendations.append({
'severity': 'critical',
'text': 'Strona laduje sie bardzo wolno. Uzytkownicy moga ja opuszczac zanim sie zaladuje.'
})
elif analysis.pagespeed_performance_score < 90:
recommendations.append({
'severity': 'warning',
'text': 'Szybkosc strony jest srednia. Mozna poprawic czas ladowania.'
})
# Meta tags
if not analysis.seo_title and not analysis.meta_title:
recommendations.append({
'severity': 'critical',
'text': 'Brak tytulu strony (meta title). Wyszukiwarki nie wiedza, czego dotyczy strona.'
})
if not analysis.seo_description and not analysis.meta_description:
recommendations.append({
'severity': 'critical',
'text': 'Brak opisu strony (meta description). To tekst wyswietlany w wynikach Google.'
})
# SSL
if analysis.has_ssl is False:
recommendations.append({
'severity': 'critical',
'text': 'Brak certyfikatu SSL (HTTPS). Przegladarki oznaczaja strone jako niebezpieczna.'
})
# Sitemap & robots
if analysis.has_sitemap is False:
recommendations.append({
'severity': 'warning',
'text': 'Brak pliku sitemap.xml. Google moze nie odkryc wszystkich podstron.'
})
if analysis.has_robots_txt is False:
recommendations.append({
'severity': 'warning',
'text': 'Brak pliku robots.txt. Nie ma instrukcji dla robotow wyszukiwarek.'
})
# Structured data
if analysis.has_structured_data is False:
recommendations.append({
'severity': 'warning',
'text': 'Brak danych strukturalnych (Schema.org). Strona nie bedzie miala rozszerzonych wynikow w Google.'
})
# Images
if analysis.images_without_alt and analysis.images_without_alt > 0:
recommendations.append({
'severity': 'warning',
'text': f'{analysis.images_without_alt} obrazkow bez opisu (alt). Problem z dostepnoscia i SEO.'
})
# H1
if analysis.h1_count is not None and analysis.h1_count != 1:
if analysis.h1_count == 0:
recommendations.append({
'severity': 'warning',
'text': 'Brak naglowka H1. Strona powinna miec dokladnie jeden glowny naglowek.'
})
elif analysis.h1_count > 1:
recommendations.append({
'severity': 'info',
'text': f'Strona ma {analysis.h1_count} naglowkow H1 (powinien byc 1).'
})
# Open Graph
if analysis.has_og_tags is False:
recommendations.append({
'severity': 'info',
'text': 'Brak tagow Open Graph. Linki udostepnione na Facebooku nie beda mialy podgladu.'
})
# Canonical
if analysis.has_canonical is False:
recommendations.append({
'severity': 'info',
'text': 'Brak tagu canonical. Moze prowadzic do duplikacji tresci w wyszukiwarkach.'
})
# Accessibility
if analysis.pagespeed_accessibility_score is not None and analysis.pagespeed_accessibility_score < 50:
recommendations.append({
'severity': 'warning',
'text': 'Niska dostepnosc strony. Osoby z niepelnosprawnosciami moga miec problem z korzystaniem.'
})
# Mobile
if analysis.is_responsive is False and analysis.is_mobile_friendly is False:
recommendations.append({
'severity': 'critical',
'text': 'Strona nie jest dostosowana do urzadzen mobilnych. Google karze takie strony w wynikach.'
})
# Security headers
if analysis.security_headers_count is not None and analysis.security_headers_count < 2:
recommendations.append({
'severity': 'info',
'text': 'Brak waznych naglowkow bezpieczenstwa (HSTS, CSP). Strona jest slabiej zabezpieczona.'
})
# All good?
if not recommendations:
recommendations.append({
'severity': 'success',
'text': 'Strona wyglada dobrze! Nie znaleziono powaznych problemow.'
})
# Add issues from seo_issues JSONB if available
if analysis.seo_issues:
for issue in analysis.seo_issues:
if isinstance(issue, dict):
recommendations.append({
'severity': issue.get('severity', 'info'),
'text': issue.get('message', issue.get('text', str(issue)))
})
return render_template('admin/seo_detail.html',
company=company,
analysis=analysis,
recommendations=recommendations
)
finally:
db.close()
# ============================================================
# GBP AUDIT ADMIN DASHBOARD
# ============================================================

View File

@ -0,0 +1,456 @@
{% extends "base.html" %}
{% block title %}Audyt SEO: {{ company.name }} - Panel Admina{% endblock %}
{% block extra_css %}
<style>
.page-header {
display: flex; justify-content: space-between; align-items: flex-start;
margin-bottom: var(--spacing-xl); flex-wrap: wrap; gap: var(--spacing-md);
}
.page-header h1 { font-size: var(--font-size-2xl); margin-bottom: var(--spacing-xs); }
.page-header .subtitle { color: var(--text-secondary); font-size: var(--font-size-sm); }
.page-header .subtitle a { color: var(--primary); text-decoration: none; }
.back-link {
display: inline-flex; align-items: center; gap: var(--spacing-xs);
color: var(--text-secondary); text-decoration: none; font-size: var(--font-size-sm);
margin-bottom: var(--spacing-md);
}
.back-link:hover { color: var(--primary); }
/* Score cards */
.scores-grid {
display: grid; grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-lg); margin-bottom: var(--spacing-2xl);
}
.score-card {
background: var(--surface); padding: var(--spacing-xl);
border-radius: var(--radius-lg); box-shadow: var(--shadow);
text-align: center;
}
.score-card .score-value {
font-size: 48px; font-weight: 700; line-height: 1;
margin-bottom: var(--spacing-xs);
}
.score-card .score-value.good { color: #16a34a; }
.score-card .score-value.medium { color: #d97706; }
.score-card .score-value.poor { color: #dc2626; }
.score-card .score-value.na { color: var(--text-muted); font-size: var(--font-size-2xl); }
.score-card .score-name { font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.score-card .score-desc { font-size: var(--font-size-xs); color: var(--text-secondary); line-height: 1.4; }
.score-ring {
width: 100px; height: 100px; margin: 0 auto var(--spacing-md);
border-radius: 50%; display: flex; align-items: center; justify-content: center;
font-size: 32px; font-weight: 700;
}
.score-ring.good { background: #dcfce7; color: #16a34a; }
.score-ring.medium { background: #fef3c7; color: #d97706; }
.score-ring.poor { background: #fee2e2; color: #dc2626; }
.score-ring.na { background: var(--border); color: var(--text-muted); }
/* Sections */
.section {
background: var(--surface); padding: var(--spacing-xl);
border-radius: var(--radius-lg); box-shadow: var(--shadow);
margin-bottom: var(--spacing-xl);
}
.section h2 {
font-size: var(--font-size-lg); margin-bottom: var(--spacing-lg);
color: var(--text-primary); border-bottom: 2px solid var(--border);
padding-bottom: var(--spacing-sm);
}
/* Recommendations */
.reco-list { list-style: none; }
.reco-item {
padding: var(--spacing-md) var(--spacing-lg);
border-left: 4px solid var(--border);
margin-bottom: var(--spacing-sm);
border-radius: 0 var(--radius) var(--radius) 0;
background: var(--background);
font-size: var(--font-size-sm);
line-height: 1.5;
}
.reco-item.critical { border-left-color: #dc2626; background: #fef2f2; }
.reco-item.warning { border-left-color: #d97706; background: #fffbeb; }
.reco-item.info { border-left-color: #2563eb; background: #eff6ff; }
.reco-item.success { border-left-color: #16a34a; background: #f0fdf4; }
.reco-severity {
display: inline-block; padding: 1px 6px; border-radius: var(--radius-sm);
font-size: 10px; font-weight: 600; text-transform: uppercase;
margin-right: var(--spacing-sm); vertical-align: middle;
}
.reco-severity.critical { background: #dc2626; color: white; }
.reco-severity.warning { background: #d97706; color: white; }
.reco-severity.info { background: #2563eb; color: white; }
.reco-severity.success { background: #16a34a; color: white; }
/* Detail grid */
.detail-grid {
display: grid; grid-template-columns: 1fr 1fr;
gap: var(--spacing-md);
}
.detail-item {
display: flex; justify-content: space-between; align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--background); border-radius: var(--radius);
}
.detail-label { color: var(--text-secondary); font-size: var(--font-size-sm); }
.detail-value { font-weight: 600; font-size: var(--font-size-sm); }
.detail-value.yes { color: #16a34a; }
.detail-value.no { color: #dc2626; }
/* Two columns */
.two-col { display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-xl); }
/* Audit info bar */
.audit-info {
display: flex; gap: var(--spacing-lg); align-items: center;
padding: var(--spacing-md) var(--spacing-lg);
background: var(--background); border-radius: var(--radius);
margin-bottom: var(--spacing-xl); font-size: var(--font-size-sm);
color: var(--text-secondary); flex-wrap: wrap;
}
.audit-info strong { color: var(--text-primary); }
@media (max-width: 768px) {
.scores-grid { grid-template-columns: repeat(2, 1fr); }
.detail-grid { grid-template-columns: 1fr; }
.two-col { grid-template-columns: 1fr; }
}
</style>
{% endblock %}
{% block content %}
<a href="{{ url_for('admin.admin_seo') }}" class="back-link">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/>
</svg>
Powrot do listy firm
</a>
<div class="page-header">
<div>
<h1>{{ company.name }}</h1>
<div class="subtitle">
{% if company.website %}
<a href="{{ company.website }}" target="_blank" rel="noopener">{{ company.website }}</a>
{% else %}
Brak strony WWW
{% endif %}
</div>
</div>
{% if analysis and current_user.can_access_admin_panel() %}
<button class="btn btn-primary btn-sm" onclick="runAudit()">Uruchom audyt ponownie</button>
{% endif %}
</div>
{% if analysis and analysis.seo_audited_at %}
<div class="audit-info">
<span>Ostatni audyt: <strong>{{ analysis.seo_audited_at.strftime('%d.%m.%Y %H:%M') }}</strong></span>
{% if analysis.cms_detected %}<span>CMS: <strong>{{ analysis.cms_detected }}</strong></span>{% endif %}
{% if analysis.hosting_provider %}<span>Hosting: <strong>{{ analysis.hosting_provider }}</strong></span>{% endif %}
{% if analysis.load_time_ms %}<span>Czas ladowania: <strong>{{ analysis.load_time_ms }}ms</strong></span>{% endif %}
</div>
{% endif %}
<!-- 4 Score Cards -->
{% macro score_class(val) %}{% if val is none %}na{% elif val >= 90 %}good{% elif val >= 50 %}medium{% else %}poor{% endif %}{% endmacro %}
<div class="scores-grid">
<div class="score-card">
<div class="score-ring {{ score_class(analysis.pagespeed_seo_score if analysis else none) }}">
{{ analysis.pagespeed_seo_score if analysis and analysis.pagespeed_seo_score is not none else '-' }}
</div>
<div class="score-name">SEO</div>
<div class="score-desc">Optymalizacja pod wyszukiwarki: meta tagi, struktura, indeksowalnosc</div>
</div>
<div class="score-card">
<div class="score-ring {{ score_class(analysis.pagespeed_performance_score if analysis else none) }}">
{{ analysis.pagespeed_performance_score if analysis and analysis.pagespeed_performance_score is not none else '-' }}
</div>
<div class="score-name">Performance</div>
<div class="score-desc">Szybkosc ladowania strony, czas do interakcji, plynnosc dzialania</div>
</div>
<div class="score-card">
<div class="score-ring {{ score_class(analysis.pagespeed_accessibility_score if analysis else none) }}">
{{ analysis.pagespeed_accessibility_score if analysis and analysis.pagespeed_accessibility_score is not none else '-' }}
</div>
<div class="score-name">Accessibility</div>
<div class="score-desc">Dostepnosc dla osob z niepelnosprawnosciami: kontrast, opisy, nawigacja klawiatura</div>
</div>
<div class="score-card">
<div class="score-ring {{ score_class(analysis.pagespeed_best_practices_score if analysis else none) }}">
{{ analysis.pagespeed_best_practices_score if analysis and analysis.pagespeed_best_practices_score is not none else '-' }}
</div>
<div class="score-name">Best Practices</div>
<div class="score-desc">Bezpieczenstwo (HTTPS), nowoczesne standardy, brak bledow technicznych</div>
</div>
</div>
<!-- Zalecenia -->
<div class="section">
<h2>Zalecenia — co poprawic</h2>
{% if recommendations %}
<ul class="reco-list">
{% for r in recommendations %}
<li class="reco-item {{ r.severity }}">
<span class="reco-severity {{ r.severity }}">
{% if r.severity == 'critical' %}KRYTYCZNE{% elif r.severity == 'warning' %}UWAGA{% elif r.severity == 'success' %}OK{% else %}INFO{% endif %}
</span>
{{ r.text }}
</li>
{% endfor %}
</ul>
{% else %}
<p class="text-muted">Brak danych do analizy. Uruchom audyt, zeby zobaczyc zalecenia.</p>
{% endif %}
</div>
{% if analysis %}
<div class="two-col">
<!-- SEO & Tresc -->
<div class="section">
<h2>SEO i tresc strony</h2>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">Tytul strony (meta title)</span>
<span class="detail-value {% if analysis.meta_title or analysis.seo_title %}yes{% else %}no{% endif %}">
{% if analysis.meta_title or analysis.seo_title %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Opis strony (meta description)</span>
<span class="detail-value {% if analysis.meta_description or analysis.seo_description %}yes{% else %}no{% endif %}">
{% if analysis.meta_description or analysis.seo_description %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Sitemap.xml</span>
<span class="detail-value {% if analysis.has_sitemap %}yes{% else %}no{% endif %}">
{% if analysis.has_sitemap %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Robots.txt</span>
<span class="detail-value {% if analysis.has_robots_txt %}yes{% else %}no{% endif %}">
{% if analysis.has_robots_txt %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Dane strukturalne (Schema.org)</span>
<span class="detail-value {% if analysis.has_structured_data %}yes{% else %}no{% endif %}">
{% if analysis.has_structured_data %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Canonical URL</span>
<span class="detail-value {% if analysis.has_canonical %}yes{% else %}no{% endif %}">
{% if analysis.has_canonical %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Open Graph (Facebook)</span>
<span class="detail-value {% if analysis.has_og_tags %}yes{% else %}no{% endif %}">
{% if analysis.has_og_tags %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Twitter Cards</span>
<span class="detail-value {% if analysis.has_twitter_cards %}yes{% else %}no{% endif %}">
{% if analysis.has_twitter_cards %}Jest{% else %}Brak{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Naglowki H1</span>
<span class="detail-value {% if analysis.h1_count == 1 %}yes{% elif analysis.h1_count is not none %}no{% endif %}">
{{ analysis.h1_count if analysis.h1_count is not none else '-' }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Naglowki H2</span>
<span class="detail-value">{{ analysis.h2_count if analysis.h2_count is not none else '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Obrazki ogolnie</span>
<span class="detail-value">{{ analysis.total_images if analysis.total_images is not none else '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Obrazki bez opisu (alt)</span>
<span class="detail-value {% if analysis.images_without_alt and analysis.images_without_alt > 0 %}no{% else %}yes{% endif %}">
{{ analysis.images_without_alt if analysis.images_without_alt is not none else '-' }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Linki wewnetrzne</span>
<span class="detail-value">{{ analysis.internal_links_count if analysis.internal_links_count is not none else '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">Linki zewnetrzne</span>
<span class="detail-value">{{ analysis.external_links_count if analysis.external_links_count is not none else '-' }}</span>
</div>
</div>
{% if analysis.meta_title or analysis.seo_title %}
<div style="margin-top: var(--spacing-lg); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius);">
<div class="detail-label" style="margin-bottom: 4px;">Tytul strony:</div>
<div style="font-size: var(--font-size-sm); color: var(--text-primary);">{{ analysis.meta_title or analysis.seo_title }}</div>
</div>
{% endif %}
{% if analysis.meta_description or analysis.seo_description %}
<div style="margin-top: var(--spacing-sm); padding: var(--spacing-md); background: var(--background); border-radius: var(--radius);">
<div class="detail-label" style="margin-bottom: 4px;">Opis strony:</div>
<div style="font-size: var(--font-size-sm); color: var(--text-primary);">{{ analysis.meta_description or analysis.seo_description }}</div>
</div>
{% endif %}
</div>
<!-- Techniczne -->
<div class="section">
<h2>Dane techniczne</h2>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">SSL (HTTPS)</span>
<span class="detail-value {% if analysis.has_ssl %}yes{% else %}no{% endif %}">
{% if analysis.has_ssl %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Responsywnosc (mobile)</span>
<span class="detail-value {% if analysis.is_responsive or analysis.is_mobile_friendly %}yes{% else %}no{% endif %}">
{% if analysis.is_responsive or analysis.is_mobile_friendly %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Indeksowalnosc</span>
<span class="detail-value {% if analysis.is_indexable %}yes{% else %}no{% endif %}">
{% if analysis.is_indexable %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Viewport (meta tag)</span>
<span class="detail-value {% if analysis.viewport_configured or analysis.has_viewport_meta %}yes{% else %}no{% endif %}">
{% if analysis.viewport_configured or analysis.has_viewport_meta %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">Jezyk strony (html lang)</span>
<span class="detail-value">{{ analysis.html_lang or '-' }}</span>
</div>
<div class="detail-item">
<span class="detail-label">CMS / System</span>
<span class="detail-value">{{ analysis.cms_detected or '-' }}</span>
</div>
{% if analysis.largest_contentful_paint_ms %}
<div class="detail-item">
<span class="detail-label">LCP — czas ladowania glownej tresci</span>
<span class="detail-value {% if analysis.largest_contentful_paint_ms <= 2500 %}yes{% elif analysis.largest_contentful_paint_ms <= 4000 %}{% else %}no{% endif %}">
{{ (analysis.largest_contentful_paint_ms / 1000)|round(1) }}s
</span>
</div>
{% endif %}
{% if analysis.cumulative_layout_shift is not none %}
<div class="detail-item">
<span class="detail-label">CLS — stabilnosc wizualna strony</span>
<span class="detail-value {% if analysis.cumulative_layout_shift|float <= 0.1 %}yes{% elif analysis.cumulative_layout_shift|float <= 0.25 %}{% else %}no{% endif %}">
{{ analysis.cumulative_layout_shift }}
</span>
</div>
{% endif %}
{% if analysis.interaction_to_next_paint_ms %}
<div class="detail-item">
<span class="detail-label">INP — szybkosc reakcji na klikniecia</span>
<span class="detail-value {% if analysis.interaction_to_next_paint_ms <= 200 %}yes{% elif analysis.interaction_to_next_paint_ms <= 500 %}{% else %}no{% endif %}">
{{ analysis.interaction_to_next_paint_ms }}ms
</span>
</div>
{% endif %}
</div>
{% if analysis.security_headers_count is not none %}
<h3 style="margin-top: var(--spacing-lg); font-size: var(--font-size-base); color: var(--text-secondary);">Zabezpieczenia</h3>
<div class="detail-grid" style="margin-top: var(--spacing-sm);">
<div class="detail-item">
<span class="detail-label">HSTS — wymuszenie HTTPS</span>
<span class="detail-value {% if analysis.has_hsts %}yes{% else %}no{% endif %}">
{% if analysis.has_hsts %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">CSP — ochrona przed wstrzykiwaniem skryptow</span>
<span class="detail-value {% if analysis.has_csp %}yes{% else %}no{% endif %}">
{% if analysis.has_csp %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">X-Frame-Options — ochrona przed osadzaniem</span>
<span class="detail-value {% if analysis.has_x_frame_options %}yes{% else %}no{% endif %}">
{% if analysis.has_x_frame_options %}Tak{% else %}Nie{% endif %}
</span>
</div>
<div class="detail-item">
<span class="detail-label">X-Content-Type — ochrona typu plikow</span>
<span class="detail-value {% if analysis.has_x_content_type_options %}yes{% else %}no{% endif %}">
{% if analysis.has_x_content_type_options %}Tak{% else %}Nie{% endif %}
</span>
</div>
</div>
{% endif %}
</div>
</div>
{% if analysis.google_rating or analysis.google_place_id %}
<div class="section">
<h2>Google Business Profile</h2>
<div class="detail-grid">
{% if analysis.google_rating %}
<div class="detail-item">
<span class="detail-label">Ocena Google</span>
<span class="detail-value">{{ analysis.google_rating }}/5 ({{ analysis.google_reviews_count or 0 }} opinii)</span>
</div>
{% endif %}
{% if analysis.google_name %}
<div class="detail-item">
<span class="detail-label">Nazwa w Google</span>
<span class="detail-value">{{ analysis.google_name }}</span>
</div>
{% endif %}
{% if analysis.google_business_status %}
<div class="detail-item">
<span class="detail-label">Status</span>
<span class="detail-value">{{ analysis.google_business_status }}</span>
</div>
{% endif %}
{% if analysis.google_phone %}
<div class="detail-item">
<span class="detail-label">Telefon z Google</span>
<span class="detail-value">{{ analysis.google_phone }}</span>
</div>
{% endif %}
</div>
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block extra_js %}
var csrfToken = '{{ csrf_token() }}';
function runAudit() {
if (!confirm('Uruchomic audyt SEO ponownie dla {{ company.name }}?')) return;
fetch('/api/seo/audit', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRFToken': csrfToken },
body: JSON.stringify({ slug: '{{ company.slug }}' })
}).then(function(r) { return r.json(); })
.then(function(data) {
if (data.success) {
alert('Audyt zakoczony! Strona zostanie odswiezona.');
location.reload();
} else {
alert('Blad: ' + (data.error || 'Nieznany blad'));
}
}).catch(function(e) { alert('Blad polaczenia: ' + e.message); });
}
{% endblock %}

View File

@ -299,6 +299,23 @@
border-color: var(--success);
}
.btn-detail {
padding: 4px 10px;
border-radius: var(--radius);
border: 1px solid var(--primary);
background: var(--primary);
color: white;
font-size: 11px;
font-weight: 600;
text-decoration: none;
transition: var(--transition);
white-space: nowrap;
}
.btn-detail:hover {
opacity: 0.9;
}
/* Category badge */
.category-badge {
display: inline-block;
@ -342,6 +359,23 @@
.legend-dot.medium { background: #fef3c7; border: 1px solid #92400e; }
.legend-dot.poor { background: #fee2e2; border: 1px solid #991b1b; }
.column-descriptions {
display: flex;
gap: var(--spacing-lg);
flex-wrap: wrap;
margin-bottom: var(--spacing-md);
font-size: var(--font-size-xs);
color: var(--text-secondary);
padding: var(--spacing-sm) var(--spacing-md);
background: #f0f9ff;
border-radius: var(--radius);
border: 1px solid #bae6fd;
}
.column-descriptions strong {
color: var(--text-primary);
}
/* Problem tags */
.problems-cell {
max-width: 160px;
@ -678,7 +712,7 @@
</div>
</div>
<!-- Legend -->
<!-- Legend & Column Descriptions -->
<div class="legend">
<div class="legend-item">
<div class="legend-dot good"></div>
@ -693,6 +727,13 @@
<span>0-49 (slaby)</span>
</div>
</div>
<div class="column-descriptions">
<span><strong>SEO</strong> — widocznosc w wyszukiwarkach</span>
<span><strong>Performance</strong> — szybkosc ladowania</span>
<span><strong>Accessibility</strong> — dostepnosc dla osob z niepelnosprawnosciami</span>
<span><strong>Best Practices</strong> — bezpieczenstwo i standardy</span>
<span><strong>Szczegoly</strong> — pelne informacje i zalecenia co poprawic</span>
</div>
<!-- Table -->
{% if companies %}
@ -825,11 +866,8 @@
</td>
<td>
<div class="action-buttons">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn-icon" title="Zobacz profil">
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
</svg>
<a href="{{ url_for('admin.admin_seo_detail', company_id=company.id) }}" class="btn-detail" title="Pelne szczegoly audytu">
Szczegoly
</a>
{% if current_user.can_access_admin_panel() %}
<button class="btn-icon audit" onclick="runSingleAudit('{{ company.slug }}')" title="Uruchom audyt SEO">