nordabiz/templates/admin/portal_seo.html
Maciej Pienczyn dd4ab4c9fc
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: portal self-audit SEO dashboard with history tracking
Add /admin/portal-seo to run SEO audits on nordabiznes.pl
using the same SEOAuditor used for company websites.
Tracks results over time for before/after comparison.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:15:33 +01:00

287 lines
10 KiB
HTML

{% extends "base.html" %}
{% block title %}SEO Portalu - Norda Biznes Partner{% endblock %}
{% block extra_css %}
<style>
.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(160px, 1fr));
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.score-card {
background: white;
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
text-align: center;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.score-card-value {
font-size: 2.5rem;
font-weight: 700;
line-height: 1;
margin-bottom: var(--spacing-xs);
}
.score-card-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
}
.score-card-delta {
font-size: var(--font-size-sm);
margin-top: var(--spacing-xs);
}
.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); }
.check-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--spacing-sm);
margin-bottom: var(--spacing-xl);
}
.check-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
background: white;
border-radius: var(--radius);
border: 1px solid var(--border);
font-size: var(--font-size-sm);
}
.check-pass { border-left: 3px solid var(--success); }
.check-fail { border-left: 3px solid var(--error); }
.check-na { border-left: 3px solid var(--border); }
.audit-history {
background: white;
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
border: 1px solid var(--border);
}
.audit-history table {
width: 100%;
border-collapse: collapse;
}
.audit-history th {
background: #f8fafc;
padding: var(--spacing-sm) var(--spacing-md);
text-align: left;
font-size: var(--font-size-sm);
font-weight: 600;
color: var(--text-secondary);
border-bottom: 2px solid var(--border);
}
.audit-history td {
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border);
font-size: var(--font-size-sm);
}
.audit-history tr:hover td {
background: #f8fafc;
}
.score-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-weight: 600;
font-size: 12px;
}
.score-badge.good { background: #d1fae5; color: #065f46; }
.score-badge.ok { background: #fef3c7; color: #92400e; }
.score-badge.bad { background: #fee2e2; color: #991b1b; }
.run-form {
display: inline-flex;
align-items: center;
gap: var(--spacing-sm);
}
</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 }} - historia zmian w czasie</p>
</div>
<form method="POST" action="{{ url_for('admin.admin_portal_seo_run') }}" class="run-form">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="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="submit" class="btn btn-primary" style="white-space: nowrap;">
Uruchom audyt
</button>
</form>
</div>
{% if audits %}
{% set latest = audits[0] %}
{% set prev = audits[1] if audits|length > 1 else None %}
<!-- Latest scores -->
<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') }}
</h2>
<div class="score-cards">
{% macro score_card(label, value, prev_value) %}
<div class="score-card">
<div class="score-card-value {{ 'score-good' if value and value >= 90 else ('score-ok' if value and value >= 50 else 'score-bad') }}">
{{ value if value is not none else '—' }}
</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 delta > 0 else ('delta-down' if delta < 0 else 'delta-same') }}">
{{ '+' if delta > 0 else '' }}{{ delta }}
</div>
{% endif %}
</div>
{% endmacro %}
{{ 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) }}
{{ score_card('On-Page', latest.on_page_score, prev.on_page_score if prev else None) }}
{{ score_card('Technical', latest.technical_score, prev.technical_score if prev else None) }}
</div>
<!-- Key checks -->
<h3 style="margin-bottom: var(--spacing-md);">Kluczowe elementy SEO</h3>
<div class="check-grid">
{% macro check_item(label, value) %}
<div class="check-item {{ 'check-pass' if value == true else ('check-fail' if value == false else 'check-na') }}">
{% if value == true %}
<span style="color: var(--success);">&#10003;</span>
{% elif value == false %}
<span style="color: var(--error);">&#10007;</span>
{% else %}
<span style="color: var(--text-secondary);">?</span>
{% endif %}
{{ label }}
</div>
{% endmacro %}
{{ check_item('robots.txt', latest.has_robots_txt) }}
{{ check_item('sitemap.xml', latest.has_sitemap) }}
{{ check_item('Canonical URL', latest.has_canonical) }}
{{ check_item('Structured Data', latest.has_structured_data) }}
{{ check_item('Open Graph', latest.has_og_tags) }}
{{ check_item('SSL', latest.has_ssl) }}
{{ check_item('Mobile Friendly', latest.is_mobile_friendly) }}
{{ check_item('HSTS', latest.has_hsts) }}
{{ check_item('CSP', latest.has_csp) }}
</div>
<!-- Core Web Vitals -->
{% if latest.lcp_ms or latest.fcp_ms or latest.cls is not none %}
<h3 style="margin-bottom: var(--spacing-md);">Core Web Vitals</h3>
<div class="score-cards" style="margin-bottom: var(--spacing-xl);">
{% if latest.lcp_ms %}
<div class="score-card">
<div class="score-card-value {{ 'score-good' if latest.lcp_ms <= 2500 else ('score-ok' if latest.lcp_ms <= 4000 else 'score-bad') }}" style="font-size: 1.8rem;">
{{ '%.1f'|format(latest.lcp_ms / 1000) }}s
</div>
<div class="score-card-label">LCP</div>
</div>
{% endif %}
{% if latest.fcp_ms %}
<div class="score-card">
<div class="score-card-value {{ 'score-good' if latest.fcp_ms <= 1800 else ('score-ok' if latest.fcp_ms <= 3000 else 'score-bad') }}" style="font-size: 1.8rem;">
{{ '%.1f'|format(latest.fcp_ms / 1000) }}s
</div>
<div class="score-card-label">FCP</div>
</div>
{% endif %}
{% if latest.cls is not none %}
<div class="score-card">
<div class="score-card-value {{ 'score-good' if latest.cls <= 0.1 else ('score-ok' if latest.cls <= 0.25 else 'score-bad') }}" style="font-size: 1.8rem;">
{{ '%.3f'|format(latest.cls) }}
</div>
<div class="score-card-label">CLS</div>
</div>
{% endif %}
{% if latest.tbt_ms %}
<div class="score-card">
<div class="score-card-value {{ 'score-good' if latest.tbt_ms <= 200 else ('score-ok' if latest.tbt_ms <= 600 else 'score-bad') }}" style="font-size: 1.8rem;">
{{ latest.tbt_ms|int }}ms
</div>
<div class="score-card-label">TBT</div>
</div>
{% endif %}
</div>
{% endif %}
{% endif %}
<!-- History table -->
<h3 style="margin-bottom: var(--spacing-md);">Historia audytów</h3>
{% if audits %}
<div class="audit-history">
<table>
<thead>
<tr>
<th>Data</th>
<th>Perf</th>
<th>SEO</th>
<th>A11y</th>
<th>BP</th>
<th>On-Page</th>
<th>Tech</th>
<th>Notatka</th>
<th></th>
</tr>
</thead>
<tbody>
{% for a in audits %}
<tr>
<td>{{ a.audited_at.strftime('%d.%m.%Y %H:%M') }}</td>
{% macro score_td(val) %}
<td>
{% if val is not none %}
<span class="score-badge {{ 'good' if val >= 90 else ('ok' if val >= 50 else 'bad') }}">{{ val }}</span>
{% else %}—{% endif %}
</td>
{% endmacro %}
{{ score_td(a.pagespeed_performance) }}
{{ score_td(a.pagespeed_seo) }}
{{ score_td(a.pagespeed_accessibility) }}
{{ score_td(a.pagespeed_best_practices) }}
{{ score_td(a.on_page_score) }}
{{ score_td(a.technical_score) }}
<td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
{{ a.notes or '' }}
</td>
<td>
<a href="{{ url_for('admin.admin_portal_seo_detail', audit_id=a.id) }}" style="color: var(--primary); font-size: var(--font-size-sm);">
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 %}