nordabiz/templates/seo_audit.html
Maciej Pienczyn 30ef2f554b
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
feat(seo): Add progress stepper and findings summary to SEO audit dashboard
Replace simple spinner with 9-step progress stepper showing audit stages
(page fetch, on-page SEO, technical SEO, PageSpeed, local SEO, citations,
content freshness, GSC, score calculation) with simulated timing and
real data enrichment from API response.

Add deterministic "Znalezione problemy" section generated from audit data
with 3 priority levels (critical/important/improvement) covering 24 rules
for SSL, meta tags, sitemap, performance, security headers, and more.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 22:23:51 +01:00

2128 lines
103 KiB
HTML

{% extends "base.html" %}
{% block title %}Audyt SEO - {{ company.name }} - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.audit-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: var(--spacing-xl);
flex-wrap: wrap;
gap: var(--spacing-md);
}
.audit-header-info h1 {
font-size: var(--font-size-2xl);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.audit-header-info p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.data-source-info {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--success-light, #dcfce7);
border-radius: var(--radius);
font-size: var(--font-size-sm);
color: var(--success, #16a34a);
}
.header-actions {
display: flex;
gap: var(--spacing-sm);
align-items: center;
}
/* Score Section */
.score-section {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
@media (max-width: 768px) {
.score-section {
grid-template-columns: 1fr;
text-align: center;
}
}
.score-circle {
width: 180px;
height: 180px;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
background: conic-gradient(
var(--score-color, var(--secondary)) calc(var(--score-percent, 0) * 3.6deg),
#e2e8f0 0deg
);
margin: 0 auto;
}
.score-circle::before {
content: '';
position: absolute;
width: 150px;
height: 150px;
border-radius: 50%;
background: var(--surface);
}
.score-value {
position: relative;
z-index: 1;
font-size: 3rem;
font-weight: 700;
line-height: 1;
}
/* Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green */
.score-value.score-excellent { color: #10b981; }
.score-value.score-good { color: #84cc16; }
.score-value.score-average { color: #f59e0b; }
.score-value.score-needs-work { color: #f97316; }
.score-value.score-poor { color: #ef4444; }
.score-label {
position: relative;
z-index: 1;
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-xs);
}
.score-details {
display: flex;
flex-direction: column;
justify-content: center;
}
.score-category {
font-size: var(--font-size-xl);
font-weight: 600;
margin-bottom: var(--spacing-sm);
}
/* Unified 5-level color scale */
.score-category.excellent { color: #10b981; }
.score-category.good { color: #84cc16; }
.score-category.average { color: #f59e0b; }
.score-category.needs-work { color: #f97316; }
.score-category.poor { color: #ef4444; }
.score-description {
color: var(--text-secondary);
line-height: 1.6;
margin-bottom: var(--spacing-md);
}
.audit-meta {
display: flex;
gap: var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
flex-wrap: wrap;
}
.audit-meta-item {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
/* Metrics Grid */
.metrics-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.metric-card {
background: var(--surface);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
box-shadow: var(--shadow-sm);
text-align: center;
border-left: 4px solid var(--border);
}
.metric-card.good { border-left-color: var(--success); }
.metric-card.medium { border-left-color: var(--warning); }
.metric-card.poor { border-left-color: var(--error); }
.metric-card.none { border-left-color: var(--border); }
.metric-icon {
width: 48px;
height: 48px;
margin: 0 auto var(--spacing-sm);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
}
.metric-icon.good { background: #dcfce7; color: #16a34a; }
.metric-icon.medium { background: #fef3c7; color: #d97706; }
.metric-icon.poor { background: #fee2e2; color: #dc2626; }
.metric-icon.none { background: var(--bg-tertiary); color: var(--text-tertiary); }
.metric-name {
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.metric-value {
font-size: var(--font-size-2xl);
font-weight: 700;
}
.metric-value.good { color: var(--success); }
.metric-value.medium { color: var(--warning); }
.metric-value.poor { color: var(--error); }
.metric-value.none { color: var(--text-tertiary); }
/* No Audit State */
.no-audit-state {
text-align: center;
padding: var(--spacing-2xl);
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
}
.no-audit-state svg {
width: 80px;
height: 80px;
color: var(--text-secondary);
opacity: 0.5;
margin-bottom: var(--spacing-md);
}
.no-audit-state h2 {
font-size: var(--font-size-xl);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
}
.no-audit-state p {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
/* Section Title */
.section-title {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
margin-bottom: var(--spacing-md);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
/* Breadcrumb */
.breadcrumb {
display: flex;
align-items: center;
gap: var(--spacing-sm);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
}
.breadcrumb a {
color: var(--primary);
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb-separator {
color: var(--border);
}
/* Loading Overlay */
.loading-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(255, 255, 255, 0.95);
z-index: 1000;
align-items: center;
justify-content: center;
flex-direction: column;
gap: var(--spacing-lg);
}
.loading-overlay.active {
display: flex;
}
.loading-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
max-width: 440px;
width: 90%;
text-align: center;
}
.loading-header {
margin-bottom: var(--spacing-lg);
}
.loading-header h3 {
font-size: var(--font-size-lg);
color: var(--text-primary);
margin-bottom: var(--spacing-xs);
}
.loading-header p {
color: var(--text-secondary);
font-size: var(--font-size-sm);
}
.loading-steps {
text-align: left;
margin-bottom: var(--spacing-lg);
}
.loading-step {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) 0;
border-bottom: 1px solid var(--border);
}
.loading-step:last-child {
border-bottom: none;
}
.step-icon {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.step-icon.pending { color: var(--text-tertiary, #9ca3af); }
.step-icon.in_progress { color: var(--primary); }
.step-icon.complete { color: var(--success); }
.step-icon.error { color: var(--error); }
.step-icon.skipped { color: var(--text-tertiary, #9ca3af); opacity: 0.5; }
.step-spinner {
width: 18px;
height: 18px;
border: 2px solid var(--border);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 1s linear infinite;
}
.step-text {
flex: 1;
font-size: var(--font-size-sm);
}
.step-text.pending { color: var(--text-tertiary, #9ca3af); }
.step-text.in_progress { color: var(--text-primary); font-weight: 500; }
.step-text.complete { color: var(--text-secondary); }
.step-text.error { color: var(--error); }
.step-text.skipped { color: var(--text-tertiary, #9ca3af); opacity: 0.6; }
.progress-bar-container {
height: 6px;
background: #e5e7eb;
border-radius: 3px;
overflow: hidden;
}
.progress-bar-fill {
height: 100%;
background: var(--primary);
border-radius: 3px;
transition: width 0.5s ease;
width: 0%;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Findings Section */
.findings-section {
background: var(--surface);
border-radius: var(--radius-lg);
box-shadow: var(--shadow);
padding: var(--spacing-xl);
margin-bottom: var(--spacing-xl);
}
.findings-header {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xs);
}
.findings-count {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 24px;
height: 24px;
padding: 0 6px;
border-radius: 12px;
font-size: var(--font-size-xs);
font-weight: 600;
background: var(--error, #ef4444);
color: white;
}
.findings-count.zero {
background: var(--success, #10b981);
}
.findings-subtitle {
color: var(--text-secondary);
font-size: var(--font-size-sm);
margin-bottom: var(--spacing-lg);
}
.findings-group {
margin-bottom: var(--spacing-md);
}
.findings-group:last-child {
margin-bottom: 0;
}
.findings-group-title {
font-size: var(--font-size-sm);
font-weight: 600;
margin-bottom: var(--spacing-sm);
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.findings-group.critical .findings-group-title { color: #dc2626; }
.findings-group.important .findings-group-title { color: #ea580c; }
.findings-group.improvement .findings-group-title { color: #d97706; }
.finding-item {
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
border-radius: var(--radius);
margin-bottom: var(--spacing-xs);
}
.findings-group.critical .finding-item {
background: #fee2e2;
border-left: 3px solid #ef4444;
}
.findings-group.important .finding-item {
background: #ffedd5;
border-left: 3px solid #f97316;
}
.findings-group.improvement .finding-item {
background: #fef3c7;
border-left: 3px solid #f59e0b;
}
.finding-icon {
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 700;
flex-shrink: 0;
margin-top: 1px;
}
.finding-icon.critical { background: #fecaca; color: #dc2626; }
.finding-icon.important { background: #fed7aa; color: #ea580c; }
.finding-icon.improvement { background: #fde68a; color: #d97706; }
.finding-title {
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-primary);
}
.finding-desc {
font-size: var(--font-size-xs);
color: var(--text-secondary);
margin-top: 2px;
}
.findings-ok {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-md);
background: #dcfce7;
border-radius: var(--radius);
color: #15803d;
font-size: var(--font-size-sm);
font-weight: 500;
}
/* Modal */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.active {
display: flex;
}
.modal-content {
background: var(--surface);
padding: var(--spacing-xl);
border-radius: var(--radius-lg);
max-width: 480px;
width: 90%;
box-shadow: var(--shadow-lg);
}
.modal-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-md);
}
.modal-icon {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.modal-icon.success { background: #dcfce7; color: #16a34a; }
.modal-icon.info { background: #dbeafe; color: #2563eb; }
.modal-title {
font-size: var(--font-size-xl);
font-weight: 600;
}
.modal-body {
color: var(--text-secondary);
margin-bottom: var(--spacing-lg);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
}
/* Website URL display */
.website-url {
display: inline-flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
background: var(--bg-tertiary);
border-radius: var(--radius);
font-size: var(--font-size-sm);
color: var(--text-secondary);
margin-top: var(--spacing-sm);
}
.website-url a {
color: var(--primary);
text-decoration: none;
}
.website-url a:hover {
text-decoration: underline;
}
</style>
{% endblock %}
{% block content %}
<!-- Breadcrumb -->
<div class="breadcrumb">
<a href="{{ url_for('index') }}">Firmy</a>
<span class="breadcrumb-separator">/</span>
<a href="{{ url_for('company_detail', company_id=company.id) }}">{{ company.name }}</a>
<span class="breadcrumb-separator">/</span>
<span>Audyt SEO</span>
</div>
<div class="audit-header">
<div class="audit-header-info">
<h1>Audyt SEO Strony WWW</h1>
<p>{{ company.name }}</p>
<div class="data-source-info">
<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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<span>Analiza SEO i wydajnosci strony WWW (Google PageSpeed Insights)</span>
</div>
{% if company.website %}
<div class="website-url">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9"/>
</svg>
<a href="{{ company.website }}" target="_blank" rel="noopener">{{ company.website }}</a>
</div>
{% endif %}
</div>
<div class="header-actions">
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn btn-outline btn-sm">
<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="M10 19l-7-7m0 0l7-7m-7 7h18"/>
</svg>
Profil firmy
</a>
{% if can_audit and company.website %}
<button class="btn btn-primary btn-sm" onclick="runAudit()" id="runAuditBtn">
<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="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Uruchom audyt
</button>
{% endif %}
</div>
</div>
{% if seo_data %}
<!-- Score Section -->
{# Unified 5-level color scale: 0-29 red, 30-49 orange, 50-69 amber, 70-89 lime, 90-100 green #}
{% set score = seo_data.seo_score or 0 %}
<div class="score-section">
<div class="score-circle" style="--score-percent: {{ score }}; --score-color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
<span class="score-value" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">{{ score }}</span>
<span class="score-label">/ 100</span>
</div>
<div class="score-details">
<div class="score-category" style="color: {% if score >= 90 %}#10b981{% elif score >= 70 %}#84cc16{% elif score >= 50 %}#f59e0b{% elif score >= 30 %}#f97316{% else %}#ef4444{% endif %};">
{% if score >= 90 %}
Doskonaly wynik SEO (Google Lighthouse)
{% elif score >= 70 %}
Dobry wynik SEO (Google Lighthouse)
{% elif score >= 50 %}
Przecietny wynik SEO (Google Lighthouse)
{% elif score >= 30 %}
Wynik SEO wymaga poprawy (Google Lighthouse)
{% else %}
Slaby wynik SEO (Google Lighthouse)
{% endif %}
</div>
<p class="score-description">
{% if score >= 90 %}
Strona jest bardzo dobrze zoptymalizowana pod katem SEO. Utrzymuj wysoki standard i monitoruj zmiany.
{% elif score >= 70 %}
Strona ma dobra optymalizacje SEO, ale sa obszary do poprawy. Skup sie na wydajnosci i dostepnosci.
{% elif score >= 50 %}
Strona wymaga pracy nad optymalizacja SEO. Warto poprawic wydajnosc i dostepnosc.
{% else %}
Strona ma powazne problemy z SEO. Priorytetowo popraw wydajnosc i optymalizacje.
{% endif %}
</p>
<p style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: var(--spacing-xs);">
Wynik pochodzi z Google PageSpeed Insights i ocenia techniczne aspekty SEO (meta tagi, robots.txt, indeksowalnosc).
Pelna ocena SEO, wlaczajac lokalne SEO i widocznosc, jest dostepna w analizie AI ponizej.
</p>
<div class="audit-meta">
<div class="audit-meta-item">
<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="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
<span>Ostatni audyt: {{ seo_data.audited_at.strftime('%d.%m.%Y %H:%M') if seo_data.audited_at else 'Brak danych' }}</span>
</div>
{% if seo_data.url %}
<div class="audit-meta-item">
<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="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1"/>
</svg>
<span>{{ seo_data.url|truncate(40) }}</span>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Metrics Grid -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
</svg>
Szczegolowe metryki
</h2>
<div class="metrics-grid">
<!-- SEO Score -->
{% set seo = seo_data.seo_score %}
{% set seo_class = 'good' if seo and seo >= 90 else ('medium' if seo and seo >= 50 else ('poor' if seo else 'none')) %}
<div class="metric-card {{ seo_class }}">
<div class="metric-icon {{ seo_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
</div>
<div class="metric-name">Wynik SEO</div>
<div class="metric-value {{ seo_class }}">{{ seo if seo else '-' }}</div>
</div>
<!-- Performance -->
{% set perf = seo_data.performance_score %}
{% set perf_class = 'good' if perf and perf >= 90 else ('medium' if perf and perf >= 50 else ('poor' if perf else 'none')) %}
<div class="metric-card {{ perf_class }}">
<div class="metric-icon {{ perf_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
</div>
<div class="metric-name">Wydajnosc</div>
<div class="metric-value {{ perf_class }}">{{ perf if perf else '-' }}</div>
</div>
<!-- Accessibility -->
{% set acc = seo_data.accessibility_score %}
{% set acc_class = 'good' if acc and acc >= 90 else ('medium' if acc and acc >= 50 else ('poor' if acc else 'none')) %}
<div class="metric-card {{ acc_class }}">
<div class="metric-icon {{ acc_class }}">
<svg width="24" height="24" 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>
</div>
<div class="metric-name">Dostepnosc</div>
<div class="metric-value {{ acc_class }}">{{ acc if acc else '-' }}</div>
</div>
<!-- Best Practices -->
{% set bp = seo_data.best_practices_score %}
{% set bp_class = 'good' if bp and bp >= 90 else ('medium' if bp and bp >= 50 else ('poor' if bp else 'none')) %}
<div class="metric-card {{ bp_class }}">
<div class="metric-icon {{ bp_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="metric-name">Best Practices</div>
<div class="metric-value {{ bp_class }}">{{ bp if bp else '-' }}</div>
</div>
</div>
<!-- Findings Section (deterministic, generated from audit data) -->
{% set findings_critical = [] %}
{% set findings_important = [] %}
{% set findings_improvement = [] %}
{# --- KRYTYCZNE --- #}
{% if seo_data.has_ssl == false %}
{% set _ = findings_critical.append({'title': 'Brak certyfikatu SSL', 'desc': 'Strona nie jest zabezpieczona protokolem HTTPS. Zainstaluj certyfikat SSL.'}) %}
{% endif %}
{% if seo_data.seo_score is not none and seo_data.seo_score < 30 %}
{% set _ = findings_critical.append({'title': 'Bardzo niski wynik SEO (' ~ seo_data.seo_score ~ '/100)', 'desc': 'Wynik SEO ponizej 30/100 wymaga pilnej interwencji.'}) %}
{% endif %}
{% if seo_data.is_indexable == false %}
{% set _ = findings_critical.append({'title': 'Strona zablokowana przed indeksowaniem', 'desc': 'Wyszukiwarki nie moga indeksowac strony. Sprawdz meta robots i robots.txt.'}) %}
{% endif %}
{% if seo_data.performance_score is not none and seo_data.performance_score < 30 %}
{% set _ = findings_critical.append({'title': 'Krytycznie niska wydajnosc (' ~ seo_data.performance_score ~ '/100)', 'desc': 'Strona laduje sie bardzo wolno. Zoptymalizuj obrazy i kod.'}) %}
{% endif %}
{# --- WAZNE --- #}
{% if not seo_data.meta_title %}
{% set _ = findings_important.append({'title': 'Brak meta title', 'desc': 'Strona nie ma ustawionego tytulu. Dodaj meta title (50-60 znakow).'}) %}
{% elif seo_data.meta_title|length < 30 %}
{% set _ = findings_important.append({'title': 'Meta title za krotki (' ~ seo_data.meta_title|length ~ ' zn.)', 'desc': 'Optymalnie 50-60 znakow. Aktualny tytul jest zbyt krotki.'}) %}
{% elif seo_data.meta_title|length > 70 %}
{% set _ = findings_important.append({'title': 'Meta title za dlugi (' ~ seo_data.meta_title|length ~ ' zn.)', 'desc': 'Optymalnie 50-60 znakow. Aktualny tytul jest zbyt dlugi i zostanie obciety w wynikach.'}) %}
{% endif %}
{% if not seo_data.meta_description %}
{% set _ = findings_important.append({'title': 'Brak meta description', 'desc': 'Strona nie ma opisu. Dodaj meta description (150-160 znakow).'}) %}
{% elif seo_data.meta_description|length < 120 %}
{% set _ = findings_important.append({'title': 'Meta description za krotki (' ~ seo_data.meta_description|length ~ ' zn.)', 'desc': 'Optymalnie 150-160 znakow. Aktualny opis jest zbyt krotki.'}) %}
{% elif seo_data.meta_description|length > 180 %}
{% set _ = findings_important.append({'title': 'Meta description za dlugi (' ~ seo_data.meta_description|length ~ ' zn.)', 'desc': 'Optymalnie 150-160 znakow. Opis zostanie obciety w wynikach wyszukiwania.'}) %}
{% endif %}
{% if seo_data.has_sitemap == false %}
{% set _ = findings_important.append({'title': 'Brak sitemap.xml', 'desc': 'Dodaj mape witryny, aby ulatwic wyszukiwarkom indeksowanie strony.'}) %}
{% endif %}
{% if seo_data.has_robots_txt == false %}
{% set _ = findings_important.append({'title': 'Brak robots.txt', 'desc': 'Dodaj plik robots.txt z instrukcjami dla robotow wyszukiwarek.'}) %}
{% endif %}
{% if seo_data.h1_count is not none and seo_data.h1_count == 0 %}
{% set _ = findings_important.append({'title': 'Brak naglowka H1', 'desc': 'Kazda strona powinna miec dokladnie jeden naglowek H1.'}) %}
{% elif seo_data.h1_count is not none and seo_data.h1_count > 1 %}
{% set _ = findings_important.append({'title': 'Wiele naglowkow H1 (' ~ seo_data.h1_count ~ ')', 'desc': 'Strona ma ' ~ seo_data.h1_count ~ ' naglowkow H1, powinien byc jeden.'}) %}
{% endif %}
{% if seo_data.images_without_alt is not none and seo_data.images_without_alt > 0 %}
{% set _ = findings_important.append({'title': 'Obrazy bez atrybutu alt (' ~ seo_data.images_without_alt ~ ')', 'desc': '' ~ seo_data.images_without_alt ~ ' obrazow nie ma opisu alternatywnego. Dodaj atrybuty alt.'}) %}
{% endif %}
{% if seo_data.lcp_ms is not none and seo_data.lcp_ms > 4000 %}
{% set _ = findings_important.append({'title': 'Wolne ladowanie (LCP ' ~ '%.1f'|format(seo_data.lcp_ms / 1000) ~ 's)', 'desc': 'Largest Contentful Paint przekracza 4 sekundy. Cel: ponizej 2.5s.'}) %}
{% endif %}
{% if seo_data.inp_ms is not none and seo_data.inp_ms > 500 %}
{% set _ = findings_important.append({'title': 'Niska interaktywnosc (INP ' ~ seo_data.inp_ms ~ 'ms)', 'desc': 'Interaction to Next Paint powinien byc ponizej 200ms.'}) %}
{% endif %}
{% if seo_data.has_local_business_schema == false %}
{% set _ = findings_important.append({'title': 'Brak schematu LocalBusiness', 'desc': 'Dodaj dane strukturalne Schema.org dla firmy lokalnej.'}) %}
{% endif %}
{% if seo_data.security_headers_count is not none and seo_data.security_headers_count < 2 %}
{% set _ = findings_important.append({'title': 'Brak naglowkow bezpieczenstwa (' ~ seo_data.security_headers_count ~ '/4)', 'desc': 'Dodaj HSTS, CSP, X-Frame-Options, X-Content-Type-Options.'}) %}
{% endif %}
{# --- DO POPRAWY --- #}
{% if seo_data.has_google_analytics == false %}
{% set _ = findings_improvement.append({'title': 'Brak Google Analytics', 'desc': 'Nie monitorujesz ruchu na stronie. Zainstaluj GA4.'}) %}
{% endif %}
{% if seo_data.has_og_tags == false %}
{% set _ = findings_improvement.append({'title': 'Brak tagow Open Graph', 'desc': 'Linki do strony nie beda ladnie wygladac w mediach spolecznosciowych.'}) %}
{% endif %}
{% if seo_data.has_twitter_cards == false %}
{% set _ = findings_improvement.append({'title': 'Brak Twitter Cards', 'desc': 'Brak tagow Twitter Card dla lepszego wygladu linkow na X.'}) %}
{% endif %}
{% if seo_data.has_canonical == false %}
{% set _ = findings_improvement.append({'title': 'Brak tagu canonical', 'desc': 'Ustaw canonical URL, aby uniknac duplikatow w indeksie.'}) %}
{% endif %}
{% if seo_data.has_google_maps_embed == false %}
{% set _ = findings_improvement.append({'title': 'Brak mapy Google na stronie', 'desc': 'Osadzenie mapy Google Maps pomaga w lokalnym SEO.'}) %}
{% endif %}
{% if seo_data.nap_on_website == false %}
{% set _ = findings_improvement.append({'title': 'Brak danych NAP na stronie', 'desc': 'Dodaj nazwe firmy, adres i telefon (NAP) na stronie.'}) %}
{% endif %}
{% if seo_data.content_freshness_score is not none and seo_data.content_freshness_score < 40 %}
{% set _ = findings_improvement.append({'title': 'Nieaktualna tresc (wynik: ' ~ seo_data.content_freshness_score ~ ')', 'desc': 'Tresc strony nie byla aktualizowana od dluzszego czasu.'}) %}
{% endif %}
{% if seo_data.modern_image_ratio is not none and seo_data.modern_image_ratio < 40 %}
{% set _ = findings_improvement.append({'title': 'Stare formaty obrazow (' ~ '%.0f'|format(seo_data.modern_image_ratio) ~ '% nowoczesnych)', 'desc': 'Konwertuj obrazy do formatow WebP lub AVIF dla lepszej wydajnosci.'}) %}
{% endif %}
{% if seo_data.word_count_homepage is not none and seo_data.word_count_homepage < 300 %}
{% set _ = findings_improvement.append({'title': 'Malo tresci na stronie (' ~ seo_data.word_count_homepage ~ ' slow)', 'desc': 'Strona ma malo tresci. Zalecane minimum to 300 slow.'}) %}
{% endif %}
{% set total_findings = findings_critical|length + findings_important|length + findings_improvement|length %}
<div class="findings-section">
<div class="findings-header">
<h2 class="section-title" style="margin-bottom: 0;">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
Znalezione problemy
</h2>
<span class="findings-count {{ 'zero' if total_findings == 0 }}">{{ total_findings }}</span>
</div>
<p class="findings-subtitle">Na podstawie audytu SEO — najwazniejsze do naprawienia</p>
{% if total_findings == 0 %}
<div class="findings-ok">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
Nie znaleziono krytycznych problemow. Twoja strona jest dobrze zoptymalizowana.
</div>
{% else %}
{% if findings_critical|length > 0 %}
<div class="findings-group critical">
<div class="findings-group-title">Krytyczne ({{ findings_critical|length }})</div>
{% for f in findings_critical %}
<div class="finding-item">
<div class="finding-icon critical">!</div>
<div>
<div class="finding-title">{{ f.title }}</div>
<div class="finding-desc">{{ f.desc }}</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if findings_important|length > 0 %}
<div class="findings-group important">
<div class="findings-group-title">Wazne ({{ findings_important|length }})</div>
{% for f in findings_important %}
<div class="finding-item">
<div class="finding-icon important">!</div>
<div>
<div class="finding-title">{{ f.title }}</div>
<div class="finding-desc">{{ f.desc }}</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if findings_improvement|length > 0 %}
<div class="findings-group improvement">
<div class="findings-group-title">Do poprawy ({{ findings_improvement|length }})</div>
{% for f in findings_improvement %}
<div class="finding-item">
<div class="finding-icon improvement">!</div>
<div>
<div class="finding-title">{{ f.title }}</div>
<div class="finding-desc">{{ f.desc }}</div>
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% endif %}
</div>
{% if seo_data.lcp_ms is not none or seo_data.inp_ms is not none or seo_data.cls is not none %}
<!-- Core Web Vitals -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
</svg>
Core Web Vitals
</h2>
<div class="metrics-grid">
{% if seo_data.lcp_ms is not none %}
{% set lcp = seo_data.lcp_ms %}
{% set lcp_class = 'good' if lcp < 2500 else ('medium' if lcp < 4000 else 'poor') %}
<div class="metric-card {{ lcp_class }}">
<div class="metric-icon {{ lcp_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div class="metric-name">LCP</div>
<div class="metric-value {{ lcp_class }}">{{ '%.1f'|format(lcp / 1000) }}s</div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">Largest Contentful Paint</div>
</div>
{% endif %}
{% if seo_data.inp_ms is not none %}
{% set inp = seo_data.inp_ms %}
{% set inp_class = 'good' if inp <= 200 else ('medium' if inp <= 500 else 'poor') %}
<div class="metric-card {{ inp_class }}">
<div class="metric-icon {{ inp_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 15l-2 5L9 9l11 4-5 2zm0 0l5 5M7.188 2.239l.777 2.897M5.136 7.965l-2.898-.777M13.95 4.05l-2.122 2.122m-5.657 5.656l-2.12 2.122"/>
</svg>
</div>
<div class="metric-name">INP</div>
<div class="metric-value {{ inp_class }}">{{ inp }}ms</div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">Interaction to Next Paint</div>
</div>
{% endif %}
{% if seo_data.cls is not none %}
{% set cls = seo_data.cls %}
{% set cls_class = 'good' if cls < 0.1 else ('medium' if cls < 0.25 else 'poor') %}
<div class="metric-card {{ cls_class }}">
<div class="metric-icon {{ cls_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>
</svg>
</div>
<div class="metric-name">CLS</div>
<div class="metric-value {{ cls_class }}">{{ '%.3f'|format(cls) }}</div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">Cumulative Layout Shift</div>
</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.crux_lcp_ms is not none %}
<!-- CrUX Field Data Section -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Dane z Chrome UX Report (realni uzytkownicy)
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<p style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin: 0 0 var(--spacing-md) 0;">Metryki p75 z realnych sesji uzytkownikow Chrome (ostatnie 28 dni)</p>
<div class="metrics-grid">
{% set crux_lcp = seo_data.crux_lcp_ms %}
{% set crux_lcp_class = 'good' if crux_lcp <= 2500 else ('medium' if crux_lcp <= 4000 else 'poor') %}
<div class="metric-card {{ crux_lcp_class }}">
<div class="metric-name">LCP (Field)</div>
<div class="metric-value {{ crux_lcp_class }}">{{ '%.1f'|format(crux_lcp / 1000) }}s</div>
{% if seo_data.crux_lcp_good_pct is not none %}
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">{{ '%.0f'|format(seo_data.crux_lcp_good_pct) }}% dobrych</div>
{% endif %}
</div>
{% if seo_data.crux_inp_ms is not none %}
{% set crux_inp = seo_data.crux_inp_ms %}
{% set crux_inp_class = 'good' if crux_inp <= 200 else ('medium' if crux_inp <= 500 else 'poor') %}
<div class="metric-card {{ crux_inp_class }}">
<div class="metric-name">INP (Field)</div>
<div class="metric-value {{ crux_inp_class }}">{{ crux_inp }}ms</div>
{% if seo_data.crux_inp_good_pct is not none %}
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-top: 4px;">{{ '%.0f'|format(seo_data.crux_inp_good_pct) }}% dobrych</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.crux_cls is not none %}
{% set crux_cls = seo_data.crux_cls %}
{% set crux_cls_class = 'good' if crux_cls < 0.1 else ('medium' if crux_cls < 0.25 else 'poor') %}
<div class="metric-card {{ crux_cls_class }}">
<div class="metric-name">CLS (Field)</div>
<div class="metric-value {{ crux_cls_class }}">{{ '%.3f'|format(crux_cls) }}</div>
</div>
{% endif %}
{% if seo_data.crux_fcp_ms is not none %}
{% set crux_fcp = seo_data.crux_fcp_ms %}
{% set crux_fcp_class = 'good' if crux_fcp <= 1800 else ('medium' if crux_fcp <= 3000 else 'poor') %}
<div class="metric-card {{ crux_fcp_class }}">
<div class="metric-name">FCP (Field)</div>
<div class="metric-value {{ crux_fcp_class }}">{{ '%.1f'|format(crux_fcp / 1000) }}s</div>
</div>
{% endif %}
{% if seo_data.crux_ttfb_ms is not none %}
{% set crux_ttfb = seo_data.crux_ttfb_ms %}
{% set crux_ttfb_class = 'good' if crux_ttfb <= 800 else ('medium' if crux_ttfb <= 1800 else 'poor') %}
<div class="metric-card {{ crux_ttfb_class }}">
<div class="metric-name">TTFB (Field)</div>
<div class="metric-value {{ crux_ttfb_class }}">{{ crux_ttfb }}ms</div>
</div>
{% endif %}
</div>
</div>
{% endif %}
<!-- Google Search Console Section -->
{% if seo_data.gsc_clicks is not none %}
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Google Search Console
<span style="font-size: var(--font-size-xs); background: #4285f4; color: white; padding: 2px 8px; border-radius: 12px; font-weight: 500; margin-left: 8px;">OAuth</span>
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<p style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin: 0 0 var(--spacing-md) 0;">Dane z Google Search za ostatnie {{ seo_data.gsc_period_days or 28 }} dni</p>
<div class="metrics-grid">
<div class="metric-card">
<div class="metric-name">Klikniecia</div>
<div class="metric-value">{{ '{:,}'.format(seo_data.gsc_clicks)|replace(',', ' ') }}</div>
</div>
<div class="metric-card">
<div class="metric-name">Wyswietlenia</div>
<div class="metric-value">{{ '{:,}'.format(seo_data.gsc_impressions)|replace(',', ' ') }}</div>
</div>
{% if seo_data.gsc_ctr is not none %}
<div class="metric-card">
<div class="metric-name">CTR</div>
<div class="metric-value">{{ '%.1f'|format(seo_data.gsc_ctr) }}%</div>
</div>
{% endif %}
{% if seo_data.gsc_avg_position is not none %}
<div class="metric-card">
<div class="metric-name">Srednia pozycja</div>
<div class="metric-value">{{ '%.1f'|format(seo_data.gsc_avg_position) }}</div>
</div>
{% endif %}
</div>
{% if seo_data.gsc_top_queries %}
<h3 style="font-size: var(--font-size-sm); font-weight: 600; margin: var(--spacing-lg) 0 var(--spacing-sm) 0;">Top zapytania w Google</h3>
<div style="overflow-x: auto;">
<table style="width: 100%; border-collapse: collapse; font-size: var(--font-size-xs);">
<thead>
<tr style="border-bottom: 2px solid var(--border-color);">
<th style="text-align: left; padding: 8px 12px; font-weight: 600;">Zapytanie</th>
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Klikniecia</th>
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Wyswietlenia</th>
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">CTR</th>
<th style="text-align: right; padding: 8px 12px; font-weight: 600;">Pozycja</th>
</tr>
</thead>
<tbody>
{% for q in seo_data.gsc_top_queries[:5] %}
<tr style="border-bottom: 1px solid var(--border-color);">
<td style="padding: 8px 12px; font-weight: 500;">{{ q.query }}</td>
<td style="text-align: right; padding: 8px 12px;">{{ q.clicks }}</td>
<td style="text-align: right; padding: 8px 12px;">{{ q.impressions }}</td>
<td style="text-align: right; padding: 8px 12px;">{{ '%.1f'|format(q.ctr) }}%</td>
<td style="text-align: right; padding: 8px 12px;">{{ '%.1f'|format(q.position) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endif %}
</div>
{% elif has_gsc_token %}
<!-- GSC connected but no data yet -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
Google Search Console
<span style="font-size: var(--font-size-xs); background: #4285f4; color: white; padding: 2px 8px; border-radius: 12px; font-weight: 500; margin-left: 8px;">OAuth</span>
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<div style="display: flex; align-items: flex-start; gap: var(--spacing-md); padding: var(--spacing-md); background: #fef3c7; border-radius: var(--radius-md); margin-bottom: var(--spacing-md);">
<svg width="24" height="24" fill="none" stroke="#d97706" viewBox="0 0 24 24" style="flex-shrink: 0; margin-top: 2px;">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4.5c-.77-.833-2.694-.833-3.464 0L3.34 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div>
<p style="margin: 0 0 4px 0; font-weight: 600; color: #92400e; font-size: var(--font-size-sm);">Konto polaczone, ale brak danych</p>
<p style="margin: 0; color: #78350f; font-size: var(--font-size-xs);">Twoja strona moze nie byc jeszcze dodana do Google Search Console. Wykonaj ponizsze kroki:</p>
</div>
</div>
<ol style="margin: 0; padding-left: 20px; color: var(--text-secondary); font-size: var(--font-size-xs); line-height: 1.8;">
<li>Wejdz na <a href="https://search.google.com/search-console" target="_blank" rel="noopener" style="color: #4285f4; font-weight: 500;">search.google.com/search-console</a></li>
<li>Kliknij <strong>Dodaj wlasciwosc</strong> i wpisz adres strony: <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 4px;">{{ company.website or '' }}</code></li>
<li>Zweryfikuj wlasciwosc (najlatwiej przez <strong>rekord DNS TXT</strong> lub <strong>tag HTML</strong>)</li>
<li>Poczekaj 2-3 dni az Google zbierze pierwsze dane</li>
<li>Uruchom ponownie audyt SEO — dane pojawia sie tutaj automatycznie</li>
</ol>
</div>
{% else %}
<!-- GSC not connected -->
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl); text-align: center;">
<svg width="32" height="32" fill="none" stroke="#9ca3af" viewBox="0 0 24 24" style="margin-bottom: var(--spacing-sm);">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<p style="color: var(--text-secondary); margin: 0 0 var(--spacing-sm) 0; font-size: var(--font-size-sm);">Polacz Google Search Console aby zobaczyc dane o widocznosci w wyszukiwarce</p>
<a href="/konto/integracje" style="display: inline-block; padding: 8px 20px; background: #4285f4; color: white; border-radius: var(--radius-md); text-decoration: none; font-size: var(--font-size-xs); font-weight: 500;">Polacz Search Console</a>
</div>
{% endif %}
{% if seo_data.local_seo_score is not none %}
<!-- Local SEO Section -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
</svg>
Local SEO
</h2>
{% set lscore = seo_data.local_seo_score %}
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<div style="display: flex; align-items: center; gap: var(--spacing-lg); margin-bottom: var(--spacing-lg);">
<div style="width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.5rem; font-weight: 700; color: {% if lscore >= 70 %}#10b981{% elif lscore >= 40 %}#f59e0b{% else %}#ef4444{% endif %}; background: conic-gradient({% if lscore >= 70 %}#10b981{% elif lscore >= 40 %}#f59e0b{% else %}#ef4444{% endif %} calc({{ lscore }} * 3.6deg), #e2e8f0 0deg); position: relative;">
<span style="position: absolute; width: 64px; height: 64px; border-radius: 50%; background: var(--surface);"></span>
<span style="position: relative; z-index: 1;">{{ lscore }}</span>
</div>
<div>
<div style="font-size: var(--font-size-lg); font-weight: 600; color: {% if lscore >= 70 %}#10b981{% elif lscore >= 40 %}#f59e0b{% else %}#ef4444{% endif %};">
{% if lscore >= 70 %}Dobry Local SEO{% elif lscore >= 40 %}Przecietny Local SEO{% else %}Slaby Local SEO{% endif %}
</div>
<p style="font-size: var(--font-size-sm); color: var(--text-secondary); margin: 0;">Ocena optymalizacji pod lokalne wyszukiwanie</p>
</div>
</div>
<!-- Local SEO Checklist -->
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-sm);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_local_business_schema else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_local_business_schema else '#ef4444' }};">{{ '✓' if seo_data.has_local_business_schema else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Schema.org LocalBusiness</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_google_maps_embed else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_google_maps_embed else '#ef4444' }};">{{ '✓' if seo_data.has_google_maps_embed else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Mapa Google na stronie</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_local_keywords else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_local_keywords else '#ef4444' }};">{{ '✓' if seo_data.has_local_keywords else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Lokalne slowa kluczowe</span>
</div>
{% if seo_data.nap_on_website %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: #dcfce7;">
<span style="color: #10b981;">&#10003;</span>
<span style="font-size: var(--font-size-sm);">NAP na stronie (Nazwa, Adres, Telefon)</span>
</div>
{% else %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: #fee2e2;">
<span style="color: #ef4444;">&#10007;</span>
<span style="font-size: var(--font-size-sm);">NAP na stronie (Nazwa, Adres, Telefon)</span>
</div>
{% endif %}
</div>
{% if seo_data.local_keywords_found %}
<div style="margin-top: var(--spacing-md);">
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary);">Znalezione slowa kluczowe:</span>
<div style="margin-top: var(--spacing-xs); display: flex; flex-wrap: wrap; gap: var(--spacing-xs);">
{% for kw in seo_data.local_keywords_found[:10] %}
<span style="padding: 2px 8px; background: #dbeafe; color: #1e40af; border-radius: var(--radius-sm); font-size: var(--font-size-xs);">{{ kw }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.content_freshness_score is not none %}
<!-- Content Freshness -->
<div class="metrics-grid" style="margin-bottom: var(--spacing-xl);">
{% set fresh = seo_data.content_freshness_score %}
{% set fresh_class = 'good' if fresh >= 70 else ('medium' if fresh >= 40 else 'poor') %}
<div class="metric-card {{ fresh_class }}">
<div class="metric-icon {{ fresh_class }}">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<div class="metric-name">Swiezosc tresci</div>
<div class="metric-value {{ fresh_class }}">{{ fresh }}</div>
</div>
{% if seo_data.last_modified_date %}
<div class="metric-card medium">
<div class="metric-icon medium">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<div class="metric-name">Ostatnia modyfikacja</div>
<div class="metric-value medium" style="font-size: var(--font-size-lg);">{{ seo_data.last_modified_date.strftime('%d.%m.%Y') }}</div>
</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.citations and seo_data.citations|length > 0 %}
<!-- Local Citations Section -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4"/>
</svg>
Cytacje lokalne ({{ seo_data.citations_count or seo_data.citations|length }})
</h2>
<div style="background: var(--surface); border-radius: var(--radius-lg); box-shadow: var(--shadow); overflow: hidden; margin-bottom: var(--spacing-xl);">
{% for citation in seo_data.citations %}
<div style="display: flex; align-items: center; justify-content: space-between; padding: var(--spacing-sm) var(--spacing-md); border-bottom: 1px solid var(--border); {% if loop.last %}border-bottom: none;{% endif %}">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<div style="width: 8px; height: 8px; border-radius: 50%; background: {{ '#10b981' if citation.status == 'found' else ('#f59e0b' if citation.status == 'incorrect' else '#ef4444') }};"></div>
<span style="font-size: var(--font-size-sm); font-weight: 500;">{{ citation.directory_name }}</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
{% if citation.status == 'found' %}
<span style="font-size: var(--font-size-xs); color: #10b981; padding: 2px 6px; background: #dcfce7; border-radius: var(--radius-sm);">Znaleziono</span>
{% if citation.listing_url %}
<a href="{{ citation.listing_url }}" target="_blank" rel="noopener" style="font-size: var(--font-size-xs); color: var(--primary);">Link</a>
{% endif %}
{% elif citation.status == 'incorrect' %}
<span style="font-size: var(--font-size-xs); color: #f59e0b; padding: 2px 6px; background: #fef3c7; border-radius: var(--radius-sm);">Bledne dane</span>
{% else %}
<span style="font-size: var(--font-size-xs); color: #ef4444; padding: 2px 6px; background: #fee2e2; border-radius: var(--radius-sm);">Nie znaleziono</span>
{% endif %}
</div>
</div>
{% endfor %}
</div>
{% endif %}
{% if seo_data.meta_title or seo_data.meta_description or seo_data.load_time_ms is not none or seo_data.word_count_homepage is not none %}
<!-- Meta Tags & Content -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z"/>
</svg>
Meta Tagi i Tresc
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
{% if seo_data.meta_title %}
{% set title_len = seo_data.meta_title|length %}
{% set title_status = 'good' if title_len >= 50 and title_len <= 60 else ('medium' if title_len >= 30 and title_len <= 70 else 'poor') %}
<div style="margin-bottom: var(--spacing-md);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary);">Meta Title</span>
<span style="font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if title_status == 'good' else ('#fef3c7' if title_status == 'medium' else '#fee2e2') }}; color: {{ '#10b981' if title_status == 'good' else ('#f59e0b' if title_status == 'medium' else '#ef4444') }};">{{ title_len }} znakow {% if title_status == 'good' %}(idealnie){% elif title_len < 50 %}(za krotki){% else %}(za dlugi){% endif %}</span>
</div>
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary); word-break: break-word;">{{ seo_data.meta_title }}</div>
</div>
{% endif %}
{% if seo_data.meta_description %}
{% set desc_len = seo_data.meta_description|length %}
{% set desc_status = 'good' if desc_len >= 150 and desc_len <= 160 else ('medium' if desc_len >= 120 and desc_len <= 180 else 'poor') %}
<div style="margin-bottom: var(--spacing-md);">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--spacing-xs);">
<span style="font-size: var(--font-size-sm); font-weight: 600; color: var(--text-primary);">Meta Description</span>
<span style="font-size: var(--font-size-xs); padding: 2px 8px; border-radius: var(--radius-sm); background: {{ '#dcfce7' if desc_status == 'good' else ('#fef3c7' if desc_status == 'medium' else '#fee2e2') }}; color: {{ '#10b981' if desc_status == 'good' else ('#f59e0b' if desc_status == 'medium' else '#ef4444') }};">{{ desc_len }} znakow {% if desc_status == 'good' %}(idealnie){% elif desc_len < 150 %}(za krotki){% else %}(za dlugi){% endif %}</span>
</div>
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm); color: var(--text-secondary); word-break: break-word;">{{ seo_data.meta_description }}</div>
</div>
{% endif %}
{% if seo_data.load_time_ms is not none or seo_data.word_count_homepage is not none %}
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: var(--spacing-sm);">
{% if seo_data.load_time_ms is not none %}
{% set lt = seo_data.load_time_ms %}
{% set lt_status = 'good' if lt < 1000 else ('medium' if lt < 3000 else 'poor') %}
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); text-align: center;">
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Czas ladowania</div>
<div style="font-size: var(--font-size-lg); font-weight: 700; color: {{ '#10b981' if lt_status == 'good' else ('#f59e0b' if lt_status == 'medium' else '#ef4444') }};">{{ '%.1f'|format(lt / 1000) }}s</div>
</div>
{% endif %}
{% if seo_data.word_count_homepage is not none %}
{% set wc = seo_data.word_count_homepage %}
{% set wc_status = 'good' if wc >= 300 else ('medium' if wc >= 100 else 'poor') %}
<div style="padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); text-align: center;">
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary);">Liczba slow</div>
<div style="font-size: var(--font-size-lg); font-weight: 700; color: {{ '#10b981' if wc_status == 'good' else ('#f59e0b' if wc_status == 'medium' else '#ef4444') }};">{{ wc }}</div>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.has_ssl is not none or seo_data.has_google_analytics is not none or seo_data.h1_count is not none or seo_data.total_images is not none %}
<!-- Technical SEO Checklist -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"/>
</svg>
Technical SEO
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-sm);">
{% if seo_data.has_ssl is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_ssl else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_ssl else '#ef4444' }};">{{ '✓' if seo_data.has_ssl else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Certyfikat SSL{% if seo_data.has_ssl and seo_data.ssl_expires_at %} (wazny do {{ seo_data.ssl_expires_at.strftime('%d.%m.%Y') }}){% endif %}</span>
</div>
{% endif %}
{% if seo_data.has_google_analytics is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_google_analytics else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_google_analytics else '#ef4444' }};">{{ '✓' if seo_data.has_google_analytics else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Google Analytics</span>
</div>
{% endif %}
{% if seo_data.has_google_tag_manager is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_google_tag_manager else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_google_tag_manager else '#ef4444' }};">{{ '✓' if seo_data.has_google_tag_manager else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Google Tag Manager</span>
</div>
{% endif %}
{% if seo_data.has_og_tags is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_og_tags else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_og_tags else '#ef4444' }};">{{ '✓' if seo_data.has_og_tags else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Open Graph Tags</span>
</div>
{% endif %}
{% if seo_data.has_twitter_cards is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_twitter_cards else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_twitter_cards else '#ef4444' }};">{{ '✓' if seo_data.has_twitter_cards else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Twitter Cards</span>
</div>
{% endif %}
{% if seo_data.h1_count is not none %}
{% set h1_ok = seo_data.h1_count == 1 %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if h1_ok else '#fef3c7' }};">
<span style="color: {{ '#10b981' if h1_ok else '#f59e0b' }};">{{ '✓' if h1_ok else '⚠' }}</span>
<span style="font-size: var(--font-size-sm);">H1: {{ seo_data.h1_count }}{% if not h1_ok %} (powinien byc 1){% endif %}{% if seo_data.h2_count is not none %}, H2: {{ seo_data.h2_count }}{% endif %}{% if seo_data.h3_count is not none %}, H3: {{ seo_data.h3_count }}{% endif %}</span>
</div>
{% endif %}
{% if seo_data.total_images is not none %}
{% set alt_ok = (seo_data.images_without_alt or 0) == 0 %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if alt_ok else '#fef3c7' }};">
<span style="color: {{ '#10b981' if alt_ok else '#f59e0b' }};">{{ '✓' if alt_ok else '⚠' }}</span>
<span style="font-size: var(--font-size-sm);">Obrazy: {{ seo_data.total_images }}{% if not alt_ok %} ({{ seo_data.images_without_alt }} bez alt){% else %} (wszystkie z alt){% endif %}</span>
</div>
{% endif %}
{% if seo_data.internal_links_count is not none or seo_data.external_links_count is not none %}
{% set broken = seo_data.broken_links_count or 0 %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if broken == 0 else '#fee2e2' }};">
<span style="color: {{ '#10b981' if broken == 0 else '#ef4444' }};">{{ '✓' if broken == 0 else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Linki: {{ seo_data.internal_links_count or 0 }} wew., {{ seo_data.external_links_count or 0 }} zew.{% if broken > 0 %}, {{ broken }} uszkodzonych{% endif %}</span>
</div>
{% endif %}
{% if seo_data.has_sitemap is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_sitemap else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_sitemap else '#ef4444' }};">{{ '✓' if seo_data.has_sitemap else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Sitemap XML</span>
</div>
{% endif %}
{% if seo_data.has_robots_txt is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_robots_txt else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_robots_txt else '#ef4444' }};">{{ '✓' if seo_data.has_robots_txt else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Robots.txt</span>
</div>
{% endif %}
{% if seo_data.has_canonical is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_canonical else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_canonical else '#ef4444' }};">{{ '✓' if seo_data.has_canonical else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Canonical URL{% if seo_data.has_canonical and seo_data.canonical_url %}: <span style="color: var(--text-secondary); word-break: break-all;">{{ seo_data.canonical_url[:60] }}{% if seo_data.canonical_url|length > 60 %}...{% endif %}</span>{% endif %}</span>
</div>
{% endif %}
{% if seo_data.is_indexable is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.is_indexable else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.is_indexable else '#ef4444' }};">{{ '✓' if seo_data.is_indexable else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Indeksowalnosc{% if not seo_data.is_indexable and seo_data.noindex_reason %} ({{ seo_data.noindex_reason }}){% endif %}</span>
</div>
{% endif %}
{% if seo_data.is_mobile_friendly is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.is_mobile_friendly else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.is_mobile_friendly else '#ef4444' }};">{{ '✓' if seo_data.is_mobile_friendly else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Mobile Friendly</span>
</div>
{% endif %}
{% if seo_data.viewport_configured is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.viewport_configured else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.viewport_configured else '#ef4444' }};">{{ '✓' if seo_data.viewport_configured else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Viewport Meta Tag</span>
</div>
{% endif %}
{% if seo_data.html_lang %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: #dcfce7;">
<span style="color: #10b981;"></span>
<span style="font-size: var(--font-size-sm);">HTML Lang: <strong>{{ seo_data.html_lang }}</strong></span>
</div>
{% endif %}
{% if seo_data.has_hreflang is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_hreflang else '#f3f4f6' }};">
<span style="color: {{ '#10b981' if seo_data.has_hreflang else '#9ca3af' }};">{{ '✓' if seo_data.has_hreflang else '—' }}</span>
<span style="font-size: var(--font-size-sm);">Hreflang{% if not seo_data.has_hreflang %} (opcjonalne){% endif %}</span>
</div>
{% endif %}
{% if seo_data.has_structured_data is not none %}
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_structured_data else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_structured_data else '#ef4444' }};">{{ '✓' if seo_data.has_structured_data else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Dane strukturalne{% if seo_data.has_structured_data and seo_data.structured_data_types %}: {{ seo_data.structured_data_types|join(', ') }}{% endif %}</span>
</div>
{% endif %}
</div>
{% if seo_data.h1_text %}
<div style="margin-top: var(--spacing-md); padding: var(--spacing-sm); background: var(--bg-tertiary); border-radius: var(--radius); font-size: var(--font-size-sm);">
<span style="font-weight: 600; color: var(--text-primary);">Tytul H1:</span>
<span style="color: var(--text-secondary);">{{ seo_data.h1_text }}</span>
</div>
{% endif %}
</div>
{% endif %}
{% if seo_data.has_hsts is not none %}
<!-- Security Headers Section -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
</svg>
Naglowki bezpieczenstwa
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
{% set sec_count = seo_data.security_headers_count or 0 %}
<div style="display: flex; align-items: center; gap: var(--spacing-md); margin-bottom: var(--spacing-md);">
<div style="font-size: var(--font-size-xl); font-weight: 700; color: {{ '#10b981' if sec_count == 4 else ('#f59e0b' if sec_count >= 2 else '#ef4444') }};">{{ sec_count }}/4</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">
{% if sec_count == 4 %}Wszystkie naglowki bezpieczenstwa skonfigurowane{% elif sec_count >= 2 %}Czesciowa ochrona — brakuje {{ 4 - sec_count }} naglowkow{% else %}Slaba ochrona — wymagana konfiguracja{% endif %}
</div>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: var(--spacing-sm);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_hsts else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_hsts else '#ef4444' }};">{{ '✓' if seo_data.has_hsts else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Strict-Transport-Security (HSTS)</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_csp else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_csp else '#ef4444' }};">{{ '✓' if seo_data.has_csp else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">Content-Security-Policy (CSP)</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_x_frame_options else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_x_frame_options else '#ef4444' }};">{{ '✓' if seo_data.has_x_frame_options else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">X-Frame-Options</span>
</div>
<div style="display: flex; align-items: center; gap: var(--spacing-sm); padding: var(--spacing-sm); border-radius: var(--radius); background: {{ '#dcfce7' if seo_data.has_x_content_type_options else '#fee2e2' }};">
<span style="color: {{ '#10b981' if seo_data.has_x_content_type_options else '#ef4444' }};">{{ '✓' if seo_data.has_x_content_type_options else '✗' }}</span>
<span style="font-size: var(--font-size-sm);">X-Content-Type-Options</span>
</div>
</div>
</div>
{% endif %}
{% if seo_data.modern_image_ratio is not none %}
<!-- Image Optimization Section -->
<h2 class="section-title">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
Optymalizacja obrazow
</h2>
<div style="background: var(--surface); padding: var(--spacing-lg); border-radius: var(--radius-lg); box-shadow: var(--shadow); margin-bottom: var(--spacing-xl);">
{% set ratio = seo_data.modern_image_ratio %}
{% set ratio_class = 'good' if ratio >= 80 else ('medium' if ratio >= 40 else 'poor') %}
<div style="display: flex; align-items: center; gap: var(--spacing-lg); margin-bottom: var(--spacing-md);">
<div>
<div style="font-size: var(--font-size-xl); font-weight: 700; color: {{ '#10b981' if ratio_class == 'good' else ('#f59e0b' if ratio_class == 'medium' else '#ef4444') }};">{{ '%.0f'|format(ratio) }}%</div>
<div style="font-size: var(--font-size-sm); color: var(--text-secondary);">nowoczesnych formatow</div>
</div>
<div style="flex: 1;">
<div style="height: 12px; background: #e5e7eb; border-radius: 6px; overflow: hidden;">
<div style="height: 100%; width: {{ ratio }}%; background: {{ '#10b981' if ratio_class == 'good' else ('#f59e0b' if ratio_class == 'medium' else '#ef4444') }}; border-radius: 6px; transition: width 0.3s;"></div>
</div>
</div>
</div>
<div style="display: flex; gap: var(--spacing-lg); font-size: var(--font-size-sm); color: var(--text-secondary);">
{% if seo_data.modern_image_count is not none %}
<span>WebP/AVIF/SVG: <strong style="color: #10b981;">{{ seo_data.modern_image_count }}</strong></span>
{% endif %}
{% if seo_data.legacy_image_count is not none %}
<span>JPG/PNG/GIF: <strong style="color: {{ '#ef4444' if seo_data.legacy_image_count > 0 else '#10b981' }};">{{ seo_data.legacy_image_count }}</strong></span>
{% endif %}
</div>
</div>
{% endif %}
{% else %}
<!-- No Audit State -->
<div class="no-audit-state">
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
</svg>
<h2>Brak danych audytu SEO</h2>
{% if company.website %}
<p>Nie przeprowadzono jeszcze audytu SEO dla strony tej firmy. Uruchom audyt, aby sprawdzic optymalizacje strony.</p>
{% if can_audit %}
<button class="btn btn-primary" onclick="runAudit()">
<svg width="20" height="20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
</svg>
Uruchom pierwszy audyt
</button>
{% endif %}
{% else %}
<p>Ta firma nie ma zdefiniowanej strony WWW. Dodaj adres strony w profilu firmy, aby moc przeprowadzic audyt SEO.</p>
<a href="{{ url_for('company_detail', company_id=company.id) }}" class="btn btn-outline">
Przejdz do profilu firmy
</a>
{% endif %}
</div>
{% endif %}
{% if seo_data %}
{% with audit_type='seo' %}
{% include 'partials/audit_ai_actions.html' %}
{% endwith %}
{% endif %}
<!-- Loading Overlay -->
<div class="loading-overlay" id="loadingOverlay">
<div class="loading-content">
<div class="loading-header">
<h3>Audyt SEO w toku...</h3>
<p>Analiza strony moze potrwac do 30 sekund</p>
</div>
<div class="loading-steps" id="loadingSteps">
<div class="loading-step" id="step-fetch">
<div class="step-icon in_progress"><div class="step-spinner"></div></div>
<span class="step-text in_progress">Pobieranie strony i walidacja HTTP</span>
</div>
<div class="loading-step" id="step-onpage">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Analiza on-page SEO (meta tagi, naglowki, obrazy)</span>
</div>
<div class="loading-step" id="step-technical">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Technical SEO (robots.txt, sitemap, canonical)</span>
</div>
<div class="loading-step" id="step-pagespeed">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Google PageSpeed Insights</span>
</div>
<div class="loading-step" id="step-local">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Analiza Local SEO</span>
</div>
<div class="loading-step" id="step-citations">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Sprawdzanie katalogow firm</span>
</div>
<div class="loading-step" id="step-freshness">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Analiza aktualnosci tresci</span>
</div>
<div class="loading-step" id="step-gsc">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Google Search Console</span>
</div>
<div class="loading-step" id="step-score">
<div class="step-icon pending"><svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg></div>
<span class="step-text pending">Obliczanie wyniku koncowego</span>
</div>
</div>
<div class="progress-bar-container">
<div class="progress-bar-fill" id="progressBarFill"></div>
</div>
</div>
</div>
<!-- Info Modal -->
<div class="modal" id="infoModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-icon success" id="modalIcon">
<svg width="24" height="24" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
</svg>
</div>
<div class="modal-title" id="modalTitle">Informacja</div>
</div>
<div class="modal-body" id="modalBody">
Tresc informacji.
</div>
<div class="modal-footer">
<button class="btn btn-primary" onclick="closeInfoModal()">OK</button>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
const csrfToken = '{{ csrf_token() }}';
const companySlug = '{{ company.slug }}';
const hasGscToken = {{ 'true' if has_gsc_token else 'false' }};
/* ============================================================
STEPPER: Step definitions & icons
============================================================ */
const seoSteps = [
{ id: 'step-fetch', label: 'Pobieranie strony i walidacja HTTP', delay: 0 },
{ id: 'step-onpage', label: 'Analiza on-page SEO (meta tagi, naglowki, obrazy)', delay: 2000 },
{ id: 'step-technical', label: 'Technical SEO (robots.txt, sitemap, canonical)', delay: 5000 },
{ id: 'step-pagespeed', label: 'Google PageSpeed Insights', delay: 8000 },
{ id: 'step-local', label: 'Analiza Local SEO', delay: 18000 },
{ id: 'step-citations', label: 'Sprawdzanie katalogow firm', delay: 20000 },
{ id: 'step-freshness', label: 'Analiza aktualnosci tresci', delay: 25000 },
{ id: 'step-gsc', label: 'Google Search Console', delay: 26000 },
{ id: 'step-score', label: 'Obliczanie wyniku koncowego', delay: 28000 }
];
const stepIcons = {
pending: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke-width="2"/></svg>',
in_progress: '<div class="step-spinner"></div>',
complete: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>',
error: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>',
skipped: '<svg width="18" height="18" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/></svg>'
};
let simulationTimers = [];
let progressInterval = null;
function updateStep(stepId, status, message) {
const el = document.getElementById(stepId);
if (!el) return;
const iconEl = el.querySelector('.step-icon');
const textEl = el.querySelector('.step-text');
iconEl.className = 'step-icon ' + status;
iconEl.innerHTML = stepIcons[status] || stepIcons.pending;
textEl.className = 'step-text ' + status;
if (message) textEl.textContent = message;
}
function resetSteps() {
seoSteps.forEach((step, i) => {
if (i === 0) {
updateStep(step.id, 'in_progress', step.label);
} else {
updateStep(step.id, 'pending', step.label);
}
});
const bar = document.getElementById('progressBarFill');
if (bar) bar.style.width = '0%';
}
function simulateSteps() {
const totalDuration = 30000;
const startTime = Date.now();
seoSteps.forEach((step, i) => {
if (i === 0) return; // first step starts immediately as in_progress
// Mark previous step complete and current step in_progress
const timer = setTimeout(() => {
// Complete previous step
if (i > 0) {
updateStep(seoSteps[i - 1].id, 'complete');
}
// Start current step
updateStep(step.id, 'in_progress');
}, step.delay);
simulationTimers.push(timer);
});
// Progress bar animation: 0% -> 90% over totalDuration
progressInterval = setInterval(() => {
const elapsed = Date.now() - startTime;
const pct = Math.min(90, (elapsed / totalDuration) * 90);
const bar = document.getElementById('progressBarFill');
if (bar) bar.style.width = pct + '%';
}, 200);
}
function stopSimulation() {
simulationTimers.forEach(t => clearTimeout(t));
simulationTimers = [];
if (progressInterval) {
clearInterval(progressInterval);
progressInterval = null;
}
}
function markAllComplete() {
seoSteps.forEach(step => {
updateStep(step.id, 'complete');
});
const bar = document.getElementById('progressBarFill');
if (bar) bar.style.width = '100%';
}
function enrichStepsFromResponse(data) {
// API response structure: data.seo_audit.{pagespeed, technical, on_page, ...}
const audit = data.seo_audit || {};
const pagespeed = audit.pagespeed || {};
const technical = audit.technical || {};
// Step 1: Fetch — show load time and HTTP status
if (technical.load_time_ms) {
const status = technical.http_status_code || 200;
updateStep('step-fetch', 'complete', 'Strona pobrana (HTTP ' + status + ', ' + technical.load_time_ms + 'ms)');
}
// Step 2: On-page — show SEO score from PageSpeed
if (pagespeed.seo_score !== undefined && pagespeed.seo_score !== null) {
updateStep('step-onpage', 'complete', 'On-page SEO: ' + pagespeed.seo_score + '/100');
}
// Step 4: PageSpeed — show performance score
if (pagespeed.performance_score !== undefined && pagespeed.performance_score !== null) {
updateStep('step-pagespeed', 'complete', 'Wydajnosc: ' + pagespeed.performance_score + '/100');
}
// Step 8: GSC
if (!hasGscToken) {
updateStep('step-gsc', 'skipped', 'Google Search Console (brak polaczenia)');
}
// Step 9: Overall score
if (audit.overall_score !== undefined && audit.overall_score !== null) {
updateStep('step-score', 'complete', 'Wynik koncowy: ' + audit.overall_score + '/100');
}
}
function showLoading() {
resetSteps();
document.getElementById('loadingOverlay').classList.add('active');
simulateSteps();
}
function hideLoading() {
stopSimulation();
document.getElementById('loadingOverlay').classList.remove('active');
}
function showInfoModal(title, body, isSuccess) {
document.getElementById('modalTitle').textContent = title;
document.getElementById('modalBody').textContent = body;
const icon = document.getElementById('modalIcon');
icon.className = 'modal-icon ' + (isSuccess ? 'success' : 'info');
document.getElementById('infoModal').classList.add('active');
}
function closeInfoModal() {
document.getElementById('infoModal').classList.remove('active');
}
document.getElementById('infoModal')?.addEventListener('click', (e) => {
if (e.target.id === 'infoModal') closeInfoModal();
});
async function runAudit() {
const btn = document.getElementById('runAuditBtn');
if (btn) btn.disabled = true;
showLoading();
try {
const response = await fetch('/api/seo/audit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ slug: companySlug })
});
const data = await response.json();
// Stop simulation, enrich with real data
stopSimulation();
if (response.ok && data.success) {
enrichStepsFromResponse(data);
markAllComplete();
const finalScore = (data.seo_audit && data.seo_audit.overall_score) || 0;
updateStep('step-score', 'complete', 'Wynik koncowy: ' + finalScore + '/100');
// Wait so user can read the completed steps
await new Promise(r => setTimeout(r, 3000));
hideLoading();
showInfoModal('Audyt zakonczony', 'Audyt SEO zostal zakonczony pomyslnie. Strona zostanie odswiezona.', true);
setTimeout(() => location.reload(), 1500);
} else {
// Mark current in_progress step as error
const currentStep = seoSteps.find(s => {
const el = document.getElementById(s.id);
return el && el.querySelector('.step-icon.in_progress');
});
if (currentStep) {
updateStep(currentStep.id, 'error', data.error || 'Blad audytu');
}
await new Promise(r => setTimeout(r, 3000));
hideLoading();
showInfoModal('Blad', data.error || 'Wystapil nieznany blad podczas audytu.', false);
if (btn) btn.disabled = false;
}
} catch (error) {
stopSimulation();
hideLoading();
showInfoModal('Blad polaczenia', 'Nie udalo sie polaczyc z serwerem: ' + error.message, false);
if (btn) btn.disabled = false;
}
}
/* ============================================================
AI AUDIT ACTIONS
============================================================ */
const companyId = {{ company.id }};
const auditType = 'seo';
function simpleMarkdown(text) {
return text
.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/^### (.+)$/gm, '<h4>$1</h4>')
.replace(/^## (.+)$/gm, '<h3>$1</h3>')
.replace(/^- (.+)$/gm, '<li>$1</li>')
.replace(/(<li>.*<\/li>)/gs, '<ul>$1</ul>')
.replace(/\n/g, '<br>');
}
async function runAIAnalysis(force) {
const prompt = document.getElementById('aiAnalyzePrompt');
const loading = document.getElementById('aiLoading');
const results = document.getElementById('aiResults');
const btn = document.getElementById('aiAnalyzeBtn');
if (btn) btn.disabled = true;
if (prompt) prompt.style.display = 'none';
if (results) results.style.display = 'none';
if (loading) loading.style.display = 'block';
// Start timer
let seconds = 0;
const timerEl = document.getElementById('aiTimer');
const timerInterval = setInterval(() => {
seconds++;
if (timerEl) timerEl.textContent = seconds + 's';
}, 1000);
try {
const response = await fetch('/api/audit/analyze', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
company_id: companyId,
audit_type: auditType,
force: !!force
})
});
const data = await response.json();
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (data.success) {
renderAIResults(data);
} else {
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad analizy AI</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(data.error || 'Nieznany blad')}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
</div>`;
results.style.display = 'block';
}
} catch (error) {
clearInterval(timerInterval);
if (loading) loading.style.display = 'none';
if (prompt) prompt.style.display = 'none';
if (btn) btn.disabled = false;
const results = document.getElementById('aiResults');
results.innerHTML = `
<div style="background: #fef2f2; border: 1px solid #fecaca; padding: var(--spacing-lg); border-radius: var(--radius-lg); text-align: center;">
<p style="color: #dc2626; font-weight: 600; margin-bottom: var(--spacing-sm);">Blad polaczenia</p>
<p style="color: #991b1b; font-size: var(--font-size-sm);">${escapeHtml(error.message)}</p>
<button class="btn btn-outline btn-sm" onclick="runAIAnalysis()" style="margin-top: var(--spacing-md);">Sprobuj ponownie</button>
</div>`;
results.style.display = 'block';
}
}
function renderAIResults(data) {
const results = document.getElementById('aiResults');
const summaryEl = document.getElementById('aiSummaryText');
const cacheInfo = document.getElementById('aiCacheInfo');
const actionsList = document.getElementById('aiActionsList');
summaryEl.textContent = data.summary || '';
if (data.cached && data.generated_at) {
const d = new Date(data.generated_at);
document.getElementById('aiCacheDate').textContent =
d.toLocaleDateString('pl-PL') + ' ' + d.toLocaleTimeString('pl-PL', {hour:'2-digit', minute:'2-digit'});
cacheInfo.style.display = 'block';
} else {
cacheInfo.style.display = 'none';
}
actionsList.innerHTML = '';
const actions = data.actions || [];
const priorityLabels = {critical: 'KRYTYCZNE', high: 'WYSOKI', medium: 'SREDNI', low: 'NISKI'};
actions.forEach((action, idx) => {
const card = document.createElement('div');
card.className = 'ai-action-card priority-' + (action.priority || 'medium');
card.id = 'ai-action-' + idx;
const impact = action.impact_score || 5;
const effort = action.effort_score || 5;
card.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: var(--spacing-sm); flex-wrap: wrap; gap: var(--spacing-xs);">
<div style="display: flex; align-items: center; gap: var(--spacing-sm);">
<span class="ai-priority-badge ${action.priority || 'medium'}">${priorityLabels[action.priority] || 'SREDNI'}</span>
<span class="ai-action-title" style="font-weight: 600; color: var(--text-primary);">${escapeHtml(action.title || '')}</span>
</div>
</div>
<p style="color: var(--text-secondary); font-size: var(--font-size-sm); margin-bottom: var(--spacing-sm);">${escapeHtml(action.description || '')}</p>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: var(--spacing-md); margin-bottom: var(--spacing-sm);">
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wplyw: ${impact}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill impact" style="width: ${impact * 10}%;"></div></div>
</div>
<div>
<div style="font-size: var(--font-size-xs); color: var(--text-tertiary); margin-bottom: 2px;">Wysilek: ${effort}/10</div>
<div class="ai-score-bar"><div class="ai-score-bar-fill effort" style="width: ${effort * 10}%;"></div></div>
</div>
</div>
<div class="ai-action-buttons">
<button class="btn btn-outline btn-sm" onclick="generateContent('${action.action_type}', ${idx})">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/></svg>
Wygeneruj tresc
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'implemented')" style="color: #10b981; border-color: #10b981;">
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>
Zrobione
</button>
<button class="btn btn-outline btn-sm" onclick="markAction(${idx}, 'dismissed')" style="color: var(--text-tertiary); border-color: var(--border);">
Odrzuc
</button>
</div>
<div id="ai-content-${idx}" style="display: none;"></div>
`;
actionsList.appendChild(card);
});
// Render comparison with previous analysis if available
if (typeof renderAIComparison === 'function') renderAIComparison(data);
results.style.display = 'block';
document.getElementById('aiActionsSection').scrollIntoView({behavior: 'smooth', block: 'start'});
// Store actions data for content generation
window._aiActions = actions;
}
async function generateContent(actionType, idx) {
const container = document.getElementById('ai-content-' + idx);
if (!container) return;
// If already has content, toggle visibility
if (container.dataset.loaded === 'true') {
container.style.display = container.style.display === 'none' ? 'block' : 'none';
return;
}
container.innerHTML = '<div style="padding: var(--spacing-md); color: var(--text-secondary); font-size: var(--font-size-sm);">Generowanie tresci...</div>';
container.style.display = 'block';
try {
const response = await fetch('/api/audit/generate-content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({
company_id: companyId,
action_type: actionType,
context: {}
})
});
const data = await response.json();
if (data.success && data.content) {
const isCode = data.content.includes('{') && (data.content.includes('<script') || data.content.includes('"@type"') || data.content.trim().startsWith('{') || data.content.trim().startsWith('<'));
if (isCode) {
container.innerHTML = `
<div class="ai-content-output">
<button class="ai-copy-btn" onclick="copyContent(this)">Kopiuj</button>
<code>${escapeHtml(data.content)}</code>
</div>
`;
} else {
container.innerHTML = `
<div style="background: var(--surface); border: 1px solid var(--border); padding: var(--spacing-md); border-radius: var(--radius); margin-top: var(--spacing-md); position: relative; line-height: 1.6; font-size: var(--font-size-sm); color: var(--text-primary);">
<button class="ai-copy-btn" style="position: absolute; top: 8px; right: 8px; background: var(--bg-tertiary); border: 1px solid var(--border); color: var(--text-secondary); padding: 4px 10px; border-radius: var(--radius-sm); font-size: var(--font-size-xs); cursor: pointer;" onclick="copyContent(this)">Kopiuj</button>
<div class="ai-markdown-content">${simpleMarkdown(data.content)}</div>
</div>
`;
}
container.dataset.loaded = 'true';
container.scrollIntoView({behavior: 'smooth', block: 'nearest'});
} else {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(data.error || 'Blad generowania')}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
} catch (error) {
container.innerHTML = `
<div style="padding: var(--spacing-sm); background: #fef2f2; border-radius: var(--radius-sm); margin-top: var(--spacing-sm);">
<span style="color: #dc2626;">${escapeHtml(error.message)}</span>
<button class="btn btn-outline btn-sm" onclick="generateContent('${actionType}', ${idx})" style="margin-left: var(--spacing-sm);">Ponow</button>
</div>`;
}
}
function copyContent(btn) {
const code = btn.parentElement.querySelector('code') || btn.parentElement.querySelector('.ai-markdown-content');
if (!code) return;
navigator.clipboard.writeText(code.textContent).then(() => {
const orig = btn.textContent;
btn.textContent = 'Skopiowano!';
setTimeout(() => { btn.textContent = orig; }, 2000);
});
}
function markAction(idx, status) {
const card = document.getElementById('ai-action-' + idx);
if (!card) return;
if (status === 'implemented') {
card.classList.add('implemented');
} else if (status === 'dismissed') {
card.classList.add('dismissed');
}
// Fire and forget status update to backend
const actions = window._aiActions || [];
if (actions[idx] && actions[idx].id) {
fetch('/api/audit/actions/' + actions[idx].id + '/status', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': csrfToken
},
body: JSON.stringify({ status: status })
}).catch(() => {});
}
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
{% endblock %}