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
Use container-full class to expand this page to full monitor width so the history table with ~35 columns is visible without scrolling. Only affects this specific page, not the rest of the portal. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
601 lines
26 KiB
HTML
601 lines
26 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}SEO Portalu - Norda Biznes Partner{% endblock %}
|
|
|
|
{% block container_class %}container-full{% endblock %}
|
|
|
|
{% block extra_css %}
|
|
<style>
|
|
.container.container-full {
|
|
max-width: 100%;
|
|
padding: 0 var(--spacing-lg);
|
|
}
|
|
.portal-seo-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: var(--spacing-xl);
|
|
flex-wrap: wrap;
|
|
gap: var(--spacing-md);
|
|
}
|
|
.score-cards {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
|
gap: var(--spacing-md);
|
|
margin-bottom: var(--spacing-xl);
|
|
}
|
|
.score-card {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-md);
|
|
text-align: center;
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.score-card-value {
|
|
font-size: 2rem;
|
|
font-weight: 700;
|
|
line-height: 1;
|
|
margin-bottom: 4px;
|
|
}
|
|
.score-card-label {
|
|
font-size: 11px;
|
|
color: var(--text-secondary);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.score-card-delta {
|
|
font-size: var(--font-size-sm);
|
|
margin-top: 2px;
|
|
}
|
|
.score-good { color: var(--success); }
|
|
.score-ok { color: var(--warning); }
|
|
.score-bad { color: var(--error); }
|
|
.delta-up { color: var(--success); }
|
|
.delta-down { color: var(--error); }
|
|
.delta-same { color: var(--text-secondary); }
|
|
|
|
.section-box {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.section-box h3 {
|
|
margin-bottom: var(--spacing-md);
|
|
font-size: var(--font-size-md);
|
|
}
|
|
|
|
.check-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
gap: var(--spacing-xs);
|
|
}
|
|
.check-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 6px 10px;
|
|
border-radius: var(--radius);
|
|
font-size: 13px;
|
|
border-left: 3px solid var(--border);
|
|
}
|
|
.check-pass { border-left-color: var(--success); background: #f0fdf4; }
|
|
.check-fail { border-left-color: var(--error); background: #fef2f2; }
|
|
|
|
.metric-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
gap: var(--spacing-sm);
|
|
}
|
|
.metric-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 8px 12px;
|
|
background: #f8fafc;
|
|
border-radius: var(--radius);
|
|
}
|
|
.metric-value { font-size: 1.3rem; font-weight: 700; }
|
|
.metric-label { font-size: 11px; color: var(--text-secondary); text-transform: uppercase; }
|
|
|
|
/* History table - scrollable */
|
|
.history-wrapper {
|
|
overflow-x: auto;
|
|
border-radius: var(--radius-lg);
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border);
|
|
}
|
|
.history-table {
|
|
width: max-content;
|
|
min-width: 100%;
|
|
border-collapse: collapse;
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
}
|
|
.history-table th, .history-table td {
|
|
padding: 6px 10px;
|
|
border-bottom: 1px solid var(--border);
|
|
text-align: center;
|
|
}
|
|
.history-table th {
|
|
background: #f1f5f9;
|
|
font-weight: 600;
|
|
color: var(--text-secondary);
|
|
position: relative;
|
|
}
|
|
.history-table thead tr:first-child th {
|
|
border-bottom: 2px solid var(--border);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.history-table thead tr:nth-child(2) th {
|
|
background: #f8fafc;
|
|
font-size: 11px;
|
|
}
|
|
.history-table tbody tr:hover td { background: #f8fafc; }
|
|
.history-table td:first-child,
|
|
.history-table th:first-child {
|
|
position: sticky;
|
|
left: 0;
|
|
z-index: 1;
|
|
background: white;
|
|
text-align: left;
|
|
border-right: 2px solid var(--border);
|
|
}
|
|
.history-table thead th:first-child { background: #f1f5f9; }
|
|
.history-table tbody tr:hover td:first-child { background: #f8fafc; }
|
|
|
|
.score-badge {
|
|
display: inline-block;
|
|
padding: 1px 6px;
|
|
border-radius: 3px;
|
|
font-weight: 600;
|
|
font-size: 11px;
|
|
}
|
|
.score-badge.good { background: #d1fae5; color: #065f46; }
|
|
.score-badge.ok { background: #fef3c7; color: #92400e; }
|
|
.score-badge.bad { background: #fee2e2; color: #991b1b; }
|
|
.bool-yes { color: var(--success); font-weight: 700; }
|
|
.bool-no { color: var(--error); font-weight: 700; }
|
|
|
|
.grp-pagespeed { border-left: 3px solid #3b82f6; }
|
|
.grp-cwv { border-left: 3px solid #8b5cf6; }
|
|
.grp-onpage { border-left: 3px solid #10b981; }
|
|
.grp-security { border-left: 3px solid #f59e0b; }
|
|
.grp-content { border-left: 3px solid #ec4899; }
|
|
.grp-scores { border-left: 3px solid #06b6d4; }
|
|
|
|
.run-form {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
}
|
|
|
|
/* Progress panel */
|
|
.audit-progress {
|
|
background: white;
|
|
border-radius: var(--radius-lg);
|
|
padding: var(--spacing-lg);
|
|
margin-bottom: var(--spacing-xl);
|
|
box-shadow: var(--shadow-sm);
|
|
border: 1px solid var(--border);
|
|
display: none;
|
|
}
|
|
.audit-progress.active { display: block; }
|
|
.progress-bar-container {
|
|
background: #e2e8f0;
|
|
border-radius: 999px;
|
|
height: 8px;
|
|
margin-bottom: var(--spacing-md);
|
|
overflow: hidden;
|
|
}
|
|
.progress-bar-fill {
|
|
background: var(--primary);
|
|
height: 100%;
|
|
border-radius: 999px;
|
|
width: 0%;
|
|
transition: width 0.4s ease;
|
|
}
|
|
.progress-steps { list-style: none; padding: 0; margin: 0; }
|
|
.progress-steps li {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--spacing-sm);
|
|
padding: 5px 0;
|
|
font-size: var(--font-size-sm);
|
|
color: var(--text-secondary);
|
|
}
|
|
.progress-steps li.active { color: var(--text-primary); font-weight: 600; }
|
|
.progress-steps li.done { color: var(--success); }
|
|
.progress-steps li.error { color: var(--error); }
|
|
.progress-steps li.warning { color: var(--warning); }
|
|
.step-icon { width: 20px; text-align: center; flex-shrink: 0; }
|
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
.spinner {
|
|
display: inline-block; width: 14px; height: 14px;
|
|
border: 2px solid var(--border); border-top-color: var(--primary);
|
|
border-radius: 50%; animation: spin 0.6s linear infinite;
|
|
}
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="portal-seo-header">
|
|
<div>
|
|
<h1 style="margin-bottom: var(--spacing-xs);">SEO Portalu</h1>
|
|
<p style="color: var(--text-secondary);">Audyt {{ portal_url }} — pełna historia zmian</p>
|
|
</div>
|
|
<div class="run-form">
|
|
<input type="text" id="audit-notes" placeholder="Notatka (opcjonalnie)" style="padding: 8px 12px; border: 1px solid var(--border); border-radius: var(--radius); font-size: var(--font-size-sm); width: 200px;">
|
|
<button type="button" id="btn-run-audit" class="btn btn-primary" style="white-space: nowrap;" onclick="startAudit()">
|
|
Uruchom audyt
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Progress panel -->
|
|
<div class="audit-progress" id="audit-progress">
|
|
<h3 style="margin-bottom: var(--spacing-md);">Audyt w toku...</h3>
|
|
<div class="progress-bar-container">
|
|
<div class="progress-bar-fill" id="progress-bar"></div>
|
|
</div>
|
|
<ul class="progress-steps" id="progress-steps">
|
|
<li data-step="1"><span class="step-icon">○</span> Inicjalizacja audytora SEO</li>
|
|
<li data-step="2"><span class="step-icon">○</span> Pobieranie strony</li>
|
|
<li data-step="3"><span class="step-icon">○</span> Analiza on-page SEO</li>
|
|
<li data-step="4"><span class="step-icon">○</span> Sprawdzanie techniczne</li>
|
|
<li data-step="5"><span class="step-icon">○</span> PageSpeed Insights API</li>
|
|
<li data-step="6"><span class="step-icon">○</span> Analiza Local SEO</li>
|
|
<li data-step="7"><span class="step-icon">○</span> Sprawdzanie cytowań</li>
|
|
<li data-step="8"><span class="step-icon">○</span> Sprawdzanie aktualności treści</li>
|
|
<li data-step="9"><span class="step-icon">○</span> Zapisywanie wyników</li>
|
|
</ul>
|
|
</div>
|
|
|
|
{% if audits %}
|
|
{% set latest = audits[0] %}
|
|
{% set prev = audits[1] if audits|length > 1 else None %}
|
|
{% set fr = latest.full_results or {} %}
|
|
{% set fr_prev = prev.full_results or {} if prev else {} %}
|
|
|
|
{# Helper macros #}
|
|
{% macro score_card(label, value, prev_value, unit='', threshold_good=90, threshold_ok=50, lower_better=false) %}
|
|
<div class="score-card">
|
|
<div class="score-card-value {% if value is not none %}{{ 'score-good' if (not lower_better and value >= threshold_good) or (lower_better and value <= threshold_good) else ('score-ok' if (not lower_better and value >= threshold_ok) or (lower_better and value <= threshold_ok) else 'score-bad') }}{% endif %}">
|
|
{% if value is not none %}{{ value }}{{ unit }}{% else %}—{% endif %}
|
|
</div>
|
|
<div class="score-card-label">{{ label }}</div>
|
|
{% if prev_value is not none and value is not none %}
|
|
{% set delta = value - prev_value %}
|
|
<div class="score-card-delta {{ 'delta-up' if (not lower_better and delta > 0) or (lower_better and delta < 0) else ('delta-down' if (not lower_better and delta < 0) or (lower_better and delta > 0) else 'delta-same') }}">
|
|
{{ '+' if delta > 0 else '' }}{{ delta }}{{ unit }}
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
{% macro check(label, value) %}
|
|
<div class="check-item {{ 'check-pass' if value else 'check-fail' }}">
|
|
{% if value %}<span style="color:var(--success)">✓</span>{% else %}<span style="color:var(--error)">✗</span>{% endif %}
|
|
{{ label }}
|
|
</div>
|
|
{% endmacro %}
|
|
|
|
<!-- Latest audit header -->
|
|
<h2 style="font-size: var(--font-size-lg); margin-bottom: var(--spacing-md); color: var(--text-secondary);">
|
|
Ostatni audyt: {{ latest.audited_at.strftime('%d.%m.%Y %H:%M') }}
|
|
{% if latest.notes %}<span style="font-size: var(--font-size-sm); font-weight: 400;"> — {{ latest.notes }}</span>{% endif %}
|
|
</h2>
|
|
|
|
<!-- PageSpeed Scores -->
|
|
<div class="section-box">
|
|
<h3>PageSpeed Insights</h3>
|
|
<div class="score-cards">
|
|
{{ score_card('Performance', latest.pagespeed_performance, prev.pagespeed_performance if prev else None) }}
|
|
{{ score_card('SEO', latest.pagespeed_seo, prev.pagespeed_seo if prev else None) }}
|
|
{{ score_card('Accessibility', latest.pagespeed_accessibility, prev.pagespeed_accessibility if prev else None) }}
|
|
{{ score_card('Best Practices', latest.pagespeed_best_practices, prev.pagespeed_best_practices if prev else None) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Core Web Vitals -->
|
|
<div class="section-box">
|
|
<h3>Core Web Vitals</h3>
|
|
<div class="score-cards">
|
|
{% set ps = fr.get('pagespeed', {}) %}
|
|
{% set cwv = ps.get('core_web_vitals', {}) %}
|
|
{% set ps_prev = fr_prev.get('pagespeed', {}) %}
|
|
{% set cwv_prev = ps_prev.get('core_web_vitals', {}) %}
|
|
|
|
{{ score_card('LCP', cwv.get('lcp_ms'), cwv_prev.get('lcp_ms'), 'ms', 2500, 4000, true) }}
|
|
{{ score_card('FCP', cwv.get('fcp_ms'), cwv_prev.get('fcp_ms'), 'ms', 1800, 3000, true) }}
|
|
{{ score_card('CLS', cwv.get('cls'), cwv_prev.get('cls'), '', 0.1, 0.25, true) }}
|
|
{{ score_card('TBT', cwv.get('tbt_ms'), cwv_prev.get('tbt_ms'), 'ms', 200, 600, true) }}
|
|
{{ score_card('TTFB', cwv.get('ttfb_ms'), cwv_prev.get('ttfb_ms'), 'ms', 800, 1800, true) }}
|
|
{{ score_card('INP', cwv.get('inp_ms'), cwv_prev.get('inp_ms'), 'ms', 200, 500, true) }}
|
|
{{ score_card('SI', cwv.get('speed_index_ms') or cwv.get('speed_index'), cwv_prev.get('speed_index_ms') or cwv_prev.get('speed_index'), 'ms', 3400, 5800, true) }}
|
|
{{ score_card('Load', fr.get('load_time_ms'), fr_prev.get('load_time_ms'), 'ms', 1000, 3000, true) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- On-Page + Technical checks -->
|
|
<div class="section-box">
|
|
<h3>Elementy SEO i bezpieczeństwo</h3>
|
|
<div class="check-grid">
|
|
{{ check('Meta Title', latest.has_meta_title) }}
|
|
{{ check('Meta Description', latest.has_meta_description) }}
|
|
{{ check('Canonical URL', latest.has_canonical) }}
|
|
{{ check('robots.txt', latest.has_robots_txt) }}
|
|
{{ check('sitemap.xml', latest.has_sitemap) }}
|
|
{{ check('Structured Data', latest.has_structured_data) }}
|
|
{{ check('Open Graph', latest.has_og_tags) }}
|
|
{{ check('SSL/HTTPS', latest.has_ssl) }}
|
|
{{ check('Indexable', fr.get('technical', {}).get('indexability', {}).get('is_indexable', None)) }}
|
|
{{ check('Viewport', fr.get('onpage', {}).get('meta_tags', {}).get('viewport') is not none) }}
|
|
{{ check('Lang Attribute', fr.get('onpage', {}).get('has_lang_attribute', false)) }}
|
|
{{ check('DOCTYPE', fr.get('onpage', {}).get('has_doctype', false)) }}
|
|
{{ check('Single H1', fr.get('onpage', {}).get('headings', {}).get('has_single_h1', false)) }}
|
|
{{ check('HSTS', latest.has_hsts) }}
|
|
{{ check('CSP', latest.has_csp) }}
|
|
{{ check('X-Frame-Options', latest.has_x_frame) }}
|
|
{{ check('X-Content-Type', latest.has_x_content_type) }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content & Link metrics -->
|
|
<div class="section-box">
|
|
<h3>Metryki treści i linków</h3>
|
|
{% set onpage = fr.get('onpage', {}) %}
|
|
{% set imgs = onpage.get('images', {}) %}
|
|
{% set links = onpage.get('links', {}) %}
|
|
{% set headings = onpage.get('headings', {}) %}
|
|
<div class="metric-grid">
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ onpage.get('word_count', '—') }}</span>
|
|
<span class="metric-label">Słowa</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ headings.get('h1_count', '—') }}</span>
|
|
<span class="metric-label">H1</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ headings.get('h2_count', '—') }}</span>
|
|
<span class="metric-label">H2</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ imgs.get('total_images', '—') }}</span>
|
|
<span class="metric-label">Obrazy</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value {{ 'score-bad' if imgs.get('images_without_alt', 0) > 0 else 'score-good' }}">{{ imgs.get('images_without_alt', '—') }}</span>
|
|
<span class="metric-label">Bez alt</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ links.get('total_links', '—') }}</span>
|
|
<span class="metric-label">Linki</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ links.get('internal_links', '—') }}</span>
|
|
<span class="metric-label">Wewnętrzne</span>
|
|
</div>
|
|
<div class="metric-item">
|
|
<span class="metric-value">{{ links.get('external_links', '—') }}</span>
|
|
<span class="metric-label">Zewnętrzne</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Local SEO, Freshness, Citations -->
|
|
<div class="section-box">
|
|
<h3>Local SEO, aktualność i cytowania</h3>
|
|
{% set local = fr.get('local_seo', {}) %}
|
|
{% set fresh = fr.get('freshness', {}) %}
|
|
{% set cit = fr.get('citations', []) %}
|
|
{% set cit_found = cit|selectattr('status', 'equalto', 'found')|list|length if cit else 0 %}
|
|
<div class="score-cards">
|
|
{{ score_card('Local SEO', local.get('local_seo_score'), None, '/100', 70, 40) }}
|
|
{{ score_card('Aktualność', fresh.get('content_freshness_score'), None, '/100', 80, 40) }}
|
|
{{ score_card('Cytowania', cit_found, None, '/' ~ (cit|length), cit|length, (cit|length)//2) }}
|
|
{{ score_card('Overall', fr.get('scores', {}).get('overall_seo'), None, '', 80, 50) }}
|
|
</div>
|
|
</div>
|
|
|
|
{% endif %}
|
|
|
|
<!-- ============================================================ -->
|
|
<!-- FULL HISTORY TABLE -->
|
|
<!-- ============================================================ -->
|
|
<h3 style="margin-bottom: var(--spacing-md);">Historia audytów</h3>
|
|
|
|
{% if audits %}
|
|
<div class="history-wrapper">
|
|
<table class="history-table">
|
|
<thead>
|
|
<tr>
|
|
<th rowspan="2" style="min-width:120px">Data</th>
|
|
<th colspan="4" class="grp-pagespeed">PageSpeed</th>
|
|
<th colspan="7" class="grp-cwv">Core Web Vitals</th>
|
|
<th colspan="7" class="grp-onpage">On-Page SEO</th>
|
|
<th colspan="4" class="grp-security">Security</th>
|
|
<th colspan="5" class="grp-content">Content</th>
|
|
<th colspan="4" class="grp-scores">Scores</th>
|
|
<th rowspan="2">Notatka</th>
|
|
<th rowspan="2"></th>
|
|
</tr>
|
|
<tr>
|
|
<!-- PageSpeed -->
|
|
<th class="grp-pagespeed">Perf</th><th class="grp-pagespeed">SEO</th><th class="grp-pagespeed">A11y</th><th class="grp-pagespeed">BP</th>
|
|
<!-- CWV -->
|
|
<th class="grp-cwv">LCP</th><th class="grp-cwv">FCP</th><th class="grp-cwv">CLS</th><th class="grp-cwv">TBT</th><th class="grp-cwv">TTFB</th><th class="grp-cwv">INP</th><th class="grp-cwv">Load</th>
|
|
<!-- On-Page -->
|
|
<th class="grp-onpage">Title</th><th class="grp-onpage">Desc</th><th class="grp-onpage">Canon</th><th class="grp-onpage">Robots</th><th class="grp-onpage">Sitemap</th><th class="grp-onpage">Schema</th><th class="grp-onpage">OG</th>
|
|
<!-- Security -->
|
|
<th class="grp-security">HSTS</th><th class="grp-security">CSP</th><th class="grp-security">X-Frame</th><th class="grp-security">X-CT</th>
|
|
<!-- Content -->
|
|
<th class="grp-content">Słowa</th><th class="grp-content">Obrazy</th><th class="grp-content">No-Alt</th><th class="grp-content">Linki</th><th class="grp-content">H1</th>
|
|
<!-- Scores -->
|
|
<th class="grp-scores">Local</th><th class="grp-scores">Fresh</th><th class="grp-scores">Cit.</th><th class="grp-scores">Overall</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{% for a in audits %}
|
|
{% set f = a.full_results or {} %}
|
|
{% set f_ps = f.get('pagespeed', {}) %}
|
|
{% set f_cwv = f_ps.get('core_web_vitals', {}) %}
|
|
{% set f_op = f.get('onpage', {}) %}
|
|
{% set f_imgs = f_op.get('images', {}) %}
|
|
{% set f_links = f_op.get('links', {}) %}
|
|
{% set f_heads = f_op.get('headings', {}) %}
|
|
{% set f_local = f.get('local_seo', {}) %}
|
|
{% set f_fresh = f.get('freshness', {}) %}
|
|
{% set f_cit = f.get('citations', []) %}
|
|
{% set f_cit_found = f_cit|selectattr('status', 'equalto', 'found')|list|length if f_cit else 0 %}
|
|
<tr>
|
|
<td>{{ a.audited_at.strftime('%d.%m %H:%M') }}</td>
|
|
|
|
{# PageSpeed scores #}
|
|
{% macro std(val, good=90, ok=50) %}<td>{% if val is not none %}<span class="score-badge {{ 'good' if val >= good else ('ok' if val >= ok else 'bad') }}">{{ val }}</span>{% else %}—{% endif %}</td>{% endmacro %}
|
|
{{ std(a.pagespeed_performance) }}
|
|
{{ std(a.pagespeed_seo) }}
|
|
{{ std(a.pagespeed_accessibility) }}
|
|
{{ std(a.pagespeed_best_practices) }}
|
|
|
|
{# CWV - lower is better #}
|
|
{% macro cwv_td(val, unit='ms', good=2500, ok=4000) %}<td>{% if val is not none %}<span class="score-badge {{ 'good' if val <= good else ('ok' if val <= ok else 'bad') }}">{{ val }}{{ unit }}</span>{% else %}—{% endif %}</td>{% endmacro %}
|
|
{{ cwv_td(f_cwv.get('lcp_ms'), 'ms', 2500, 4000) }}
|
|
{{ cwv_td(f_cwv.get('fcp_ms'), 'ms', 1800, 3000) }}
|
|
{{ cwv_td(f_cwv.get('cls'), '', 0.1, 0.25) }}
|
|
{{ cwv_td(f_cwv.get('tbt_ms'), 'ms', 200, 600) }}
|
|
{{ cwv_td(f_cwv.get('ttfb_ms'), 'ms', 800, 1800) }}
|
|
{{ cwv_td(f_cwv.get('inp_ms'), 'ms', 200, 500) }}
|
|
{{ cwv_td(f.get('load_time_ms'), 'ms', 1000, 3000) }}
|
|
|
|
{# On-Page booleans #}
|
|
{% macro bool_td(val) %}<td>{% if val %}<span class="bool-yes">✓</span>{% elif val is sameas false %}<span class="bool-no">✗</span>{% else %}—{% endif %}</td>{% endmacro %}
|
|
{{ bool_td(a.has_meta_title) }}
|
|
{{ bool_td(a.has_meta_description) }}
|
|
{{ bool_td(a.has_canonical) }}
|
|
{{ bool_td(a.has_robots_txt) }}
|
|
{{ bool_td(a.has_sitemap) }}
|
|
{{ bool_td(a.has_structured_data) }}
|
|
{{ bool_td(a.has_og_tags) }}
|
|
|
|
{# Security booleans #}
|
|
{{ bool_td(a.has_hsts) }}
|
|
{{ bool_td(a.has_csp) }}
|
|
{{ bool_td(a.has_x_frame) }}
|
|
{{ bool_td(a.has_x_content_type) }}
|
|
|
|
{# Content metrics #}
|
|
<td>{{ f_op.get('word_count', '—') }}</td>
|
|
<td>{{ f_imgs.get('total_images', '—') }}</td>
|
|
<td>{% if f_imgs.get('images_without_alt') is not none %}<span class="{{ 'bool-no' if f_imgs.get('images_without_alt', 0) > 0 else 'bool-yes' }}">{{ f_imgs.get('images_without_alt') }}</span>{% else %}—{% endif %}</td>
|
|
<td>{{ f_links.get('total_links', '—') }}</td>
|
|
<td>{{ f_heads.get('h1_count', '—') }}</td>
|
|
|
|
{# Scores #}
|
|
{{ std(f_local.get('local_seo_score'), 70, 40) }}
|
|
{{ std(f_fresh.get('content_freshness_score'), 80, 40) }}
|
|
<td>{% if f_cit %}{{ f_cit_found }}/{{ f_cit|length }}{% else %}—{% endif %}</td>
|
|
{{ std(f.get('scores', {}).get('overall_seo'), 80, 50) }}
|
|
|
|
<td style="max-width:120px; overflow:hidden; text-overflow:ellipsis;" title="{{ a.notes or '' }}">{{ a.notes or '' }}</td>
|
|
<td><a href="{{ url_for('admin.admin_portal_seo_detail', audit_id=a.id) }}" style="color:var(--primary);">Szczegóły</a></td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% else %}
|
|
<div style="text-align: center; padding: var(--spacing-2xl); color: var(--text-secondary);">
|
|
<p>Brak audytów. Kliknij "Uruchom audyt" aby wykonać pierwszy audyt SEO portalu.</p>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% endblock %}
|
|
|
|
{% block extra_js %}
|
|
function startAudit() {
|
|
var btn = document.getElementById('btn-run-audit');
|
|
var panel = document.getElementById('audit-progress');
|
|
var bar = document.getElementById('progress-bar');
|
|
var stepsList = document.getElementById('progress-steps');
|
|
var notes = document.getElementById('audit-notes').value;
|
|
|
|
btn.disabled = true;
|
|
btn.textContent = 'Trwa audyt...';
|
|
panel.classList.add('active');
|
|
|
|
var items = stepsList.querySelectorAll('li');
|
|
items.forEach(function(li) {
|
|
li.className = '';
|
|
li.querySelector('.step-icon').innerHTML = '○';
|
|
});
|
|
bar.style.width = '0%';
|
|
|
|
var url = '{{ url_for("admin.admin_portal_seo_run_stream") }}' + '?notes=' + encodeURIComponent(notes);
|
|
var source = new EventSource(url);
|
|
|
|
source.onmessage = function(e) {
|
|
var data = JSON.parse(e.data);
|
|
|
|
if (data.status === 'complete') {
|
|
source.close();
|
|
bar.style.width = '100%';
|
|
btn.textContent = 'Ukończono!';
|
|
panel.querySelector('h3').textContent = 'Audyt zakończony';
|
|
setTimeout(function() { window.location.reload(); }, 1500);
|
|
return;
|
|
}
|
|
|
|
if (data.status === 'error' && !data.step) {
|
|
source.close();
|
|
btn.disabled = false;
|
|
btn.textContent = 'Uruchom audyt';
|
|
panel.querySelector('h3').textContent = 'Błąd: ' + (data.message || 'Nieznany');
|
|
return;
|
|
}
|
|
|
|
if (data.step) {
|
|
bar.style.width = Math.round((data.step / data.total) * 100) + '%';
|
|
var li = stepsList.querySelector('li[data-step="' + data.step + '"]');
|
|
if (!li) return;
|
|
var icon = li.querySelector('.step-icon');
|
|
var label = li.childNodes[li.childNodes.length - 1];
|
|
|
|
if (data.status === 'running') {
|
|
li.className = 'active';
|
|
icon.innerHTML = '<span class="spinner"></span>';
|
|
} else if (data.status === 'done') {
|
|
li.className = 'done';
|
|
icon.innerHTML = '✓';
|
|
label.textContent = ' ' + data.message;
|
|
} else if (data.status === 'error') {
|
|
li.className = 'error';
|
|
icon.innerHTML = '✗';
|
|
label.textContent = ' ' + data.message;
|
|
} else if (data.status === 'warning') {
|
|
li.className = 'warning';
|
|
icon.innerHTML = '⚠';
|
|
label.textContent = ' ' + data.message;
|
|
} else if (data.status === 'skipped') {
|
|
li.className = '';
|
|
icon.innerHTML = '—';
|
|
label.textContent = ' ' + data.message;
|
|
}
|
|
}
|
|
};
|
|
|
|
source.onerror = function() {
|
|
source.close();
|
|
btn.disabled = false;
|
|
btn.textContent = 'Uruchom audyt';
|
|
if (bar.style.width === '100%') return;
|
|
panel.querySelector('h3').textContent = 'Połączenie przerwane';
|
|
};
|
|
}
|
|
{% endblock %}
|