Files
crawler-seo/dashboard_api.py
T

576 lines
38 KiB
Python

import sqlite3
import json
import glob
import os
from fastapi import FastAPI, HTTPException, Query
from fastapi.responses import HTMLResponse, StreamingResponse
from typing import List, Optional
import io
import csv
app = FastAPI(title="Crawler SEO Dashboard API")
def get_db_conn(db_name: str):
# Sprawdź czy db_name zawiera już katalog, jeśli nie - dodaj scans/
if not db_name.startswith("scans/"):
db_path = os.path.join("scans", db_name)
else:
db_path = db_name
if not os.path.exists(db_path):
raise HTTPException(status_code=404, detail=f"Baza danych nie istnieje: {db_path}")
conn = sqlite3.connect(db_path)
conn.row_factory = sqlite3.Row
return conn
@app.get("/", response_class=HTMLResponse)
def get_dashboard():
return """
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Crawler SEO Dashboard</title>
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@0.321.0/dist/umd/lucide.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; background: #0f172a; color: #f1f5f9; margin: 0; }
.glass { background: rgba(30, 41, 59, 0.7); backdrop-filter: blur(12px); border: 1px solid rgba(255,255,255,0.1); }
::-webkit-scrollbar { width: 8px; }
::-webkit-scrollbar-track { background: #0f172a; }
::-webkit-scrollbar-thumb { background: #334155; border-radius: 4px; }
.animate-fade-in { animation: fadeIn 0.4s ease-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState, useEffect } = React;
const SCHEMA_DEFS = {
'Product': [
{ field: 'name', label: 'Nazwa produktu', status: 'req' },
{ field: 'image', label: 'Zdjęcie', status: 'req' },
{ field: 'offers.price', label: 'Cena', status: 'req' },
{ field: 'offers.priceCurrency', label: 'Waluta', status: 'req' },
{ field: 'description', label: 'Opis', status: 'opt' },
{ field: 'sku', label: 'SKU', status: 'warn' },
{ field: 'brand.name', label: 'Marka', status: 'warn' },
{ field: 'aggregateRating.ratingValue', label: 'Ocena', status: 'opt' },
{ field: 'offers.availability', label: 'Dostępność', status: 'warn' }
],
'BreadcrumbList': [
{ field: 'itemListElement', label: 'Elementy listy', status: 'req' }
]
};
function App() {
const [dbs, setDbs] = useState([]);
const [selectedDb, setSelectedDb] = useState('');
const [stats, setStats] = useState(null);
const [pages, setPages] = useState([]);
const [loading, setLoading] = useState(false);
const [filter, setFilter] = useState('all');
const [selectedPage, setSelectedPage] = useState(null);
const [analysisData, setAnalysisData] = useState({ schemas: [], images: [] });
const [activeTab, setActiveTab] = useState('schema');
const [mainTab, setMainTab] = useState('pages');
const [translations, setTranslations] = useState({ langs: [], data: [] });
const [sortConfig, setSortConfig] = useState({ key: 'id', direction: 'desc' });
useEffect(() => {
fetch('/api/list-dbs').then(res => res.json()).then(data => {
setDbs(data);
if (data.length > 0) setSelectedDb(data[0]);
});
}, []);
useEffect(() => {
if (selectedDb) {
setLoading(true);
const endpoints = mainTab === 'pages'
? [fetch(`/api/stats?db=${selectedDb}`), fetch(`/api/pages?db=${selectedDb}&status_type=${filter}`)]
: [fetch(`/api/stats?db=${selectedDb}`), fetch(`/api/translations?db=${selectedDb}`)];
Promise.all(endpoints).then(async ([resStats, resData]) => {
const s = await resStats.json();
const d = await resData.json();
setStats(s);
if (mainTab === 'pages') setPages(d);
else setTranslations(d);
setLoading(false);
}).catch(() => setLoading(false));
}
}, [selectedDb, filter, mainTab]);
useEffect(() => {
if (window.lucide) window.lucide.createIcons();
}, [pages, stats, selectedPage, sortConfig, loading, activeTab, mainTab, translations]);
const requestSort = (key) => {
let direction = 'asc';
if (sortConfig.key === key && sortConfig.direction === 'asc') direction = 'desc';
setSortConfig({ key, direction });
};
const getSortedPages = () => {
if (!pages) return [];
const sortable = [...pages];
sortable.sort((a, b) => {
let valA = a[sortConfig.key] ?? 0;
let valB = b[sortConfig.key] ?? 0;
if (sortConfig.key === 'schema_status') {
valA = ((a.schema_critical || 0) * 100) + (a.schema_warnings || 0);
valB = ((b.schema_critical || 0) * 100) + (b.schema_warnings || 0);
}
if (valA < valB) return sortConfig.direction === 'asc' ? -1 : 1;
if (valA > valB) return sortConfig.direction === 'asc' ? 1 : -1;
return 0;
});
return sortable;
};
const viewAnalysis = (page) => {
fetch(`/api/analysis/${page.id}?db=${selectedDb}`).then(res => res.json()).then(data => {
setAnalysisData(data);
setSelectedPage(page);
setActiveTab('schema');
});
};
const getVal = (obj, path) => path.split('.').reduce((acc, part) => acc && acc[part], obj);
const sortedPages = getSortedPages();
return (
<div className="min-h-screen p-6 max-w-7xl mx-auto animate-fade-in">
<header className="flex flex-col md:flex-row justify-between items-start md:items-center mb-10 gap-4">
<div>
<h1 className="text-3xl font-extrabold bg-gradient-to-r from-blue-400 to-emerald-400 bg-clip-text text-transparent uppercase tracking-tight">
SEO Audit Dashboard
</h1>
<p className="text-slate-500 text-sm mt-1 font-medium">Monitoring techniczny i audyt wielojęzyczności</p>
</div>
<div className="flex items-center space-x-3 bg-slate-800/50 p-1.5 rounded-2xl border border-slate-700">
<span className="text-[10px] text-slate-500 font-black px-3 uppercase tracking-widest">Baza danych</span>
<select value={selectedDb} onChange={(e) => setSelectedDb(e.target.value)} className="bg-transparent text-slate-200 p-2 pr-8 rounded-xl outline-none text-xs font-bold cursor-pointer">
{dbs.map(db => <option key={db} value={db} className="bg-slate-900">{db}</option>)}
</select>
</div>
</header>
{stats && (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-10">
{[
{ t: 'Strony', v: stats.total_pages, i: 'layers', c: 'blue' },
{ t: 'Błędy HTTP', v: stats.errors, i: 'alert-circle', c: 'red' },
{ t: 'Obiekty Schema', v: stats.schema_objects, i: 'code', c: 'emerald' },
{ t: 'Błędy Tłumaczeń', v: stats.translation_errors || 0, i: 'globe', c: 'amber' }
].map((s, idx) => (
<div key={idx} className="glass p-5 rounded-2xl border border-white/5 shadow-lg flex items-center space-x-4">
<div className={`p-2.5 rounded-xl bg-${s.c}-500/20 text-${s.c}-400`}><i data-lucide={s.i} className="w-5 h-5"></i></div>
<div><p className="text-[10px] text-slate-500 font-bold uppercase tracking-wider">{s.t}</p><p className="text-xl font-bold text-slate-100">{s.v}</p></div>
</div>
))}
</div>
)}
<div className="flex bg-slate-800/30 rounded-2xl p-1 mb-8 w-fit border border-white/5">
<button onClick={() => setMainTab('pages')} className={`px-6 py-2 rounded-xl text-xs font-bold transition-all ${mainTab==='pages' ? 'bg-blue-600 text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}>AUDYT TECHNICZNY</button>
<button onClick={() => setMainTab('translations')} className={`px-6 py-2 rounded-xl text-xs font-bold transition-all ${mainTab==='translations' ? 'bg-blue-600 text-white shadow-lg' : 'text-slate-500 hover:text-slate-300'}`}>AUDYT TŁUMACZEŃ</button>
</div>
{mainTab === 'pages' ? (
<div>
<div className="flex flex-wrap items-center justify-between gap-4 mb-6">
<div className="flex flex-wrap gap-2">
{[{id:'all',l:'Wszystkie'},{id:'error',l:'Błędy'},{id:'slow',l:'Wolne'},{id:'images',l:'Obrazy'}].map(f => (
<button key={f.id} onClick={() => setFilter(f.id)} className={`px-5 py-1.5 rounded-xl text-xs font-bold transition-all ${filter===f.id ? 'bg-blue-600 text-white shadow-lg' : 'glass text-slate-500 hover:text-slate-300'}`}>
{f.l.toUpperCase()}
</button>
))}
</div>
<a href={`/api/export-csv?db=${selectedDb}&status_type=${filter}`} download className="bg-emerald-600/20 hover:bg-emerald-600 text-emerald-400 hover:text-white px-5 py-1.5 rounded-xl text-xs font-bold transition-all flex items-center space-x-2 border border-emerald-600/30">
<i data-lucide="download" className="w-4 h-4"></i><span>EKSPORTUJ CSV</span>
</a>
</div>
<div className="glass rounded-3xl border border-white/5 shadow-2xl">
<div className="overflow-auto max-h-[75vh]">
<table className="w-full text-left border-collapse">
<thead className="bg-slate-900 sticky top-0 z-10 shadow-md">
<tr className="text-[10px] font-black text-slate-400 uppercase tracking-[0.15em]">
{[{k:'status',l:'Status'},{k:'lang',l:'Język'},{k:'url',l:'URL'},{k:'total_time',l:'Czas'},{k:'schema_status',l:'Schema'}].map(col => (
<th key={col.k} onClick={()=>requestSort(col.k)} className="p-4 cursor-pointer hover:bg-white/5 transition">
<div className="flex items-center space-x-1"><span>{col.l}</span>{sortConfig.key === col.k && <i data-lucide={sortConfig.direction==='asc'?'chevron-up':'chevron-down'} className="w-3 h-3 text-blue-400"></i>}</div>
</th>
))}
<th className="p-4 text-right">Akcje</th>
</tr>
</thead>
<tbody className="text-sm">
{loading ? (
<tr><td colSpan="6" className="p-20 text-center text-slate-600 font-bold animate-pulse uppercase tracking-[0.2em]">Pobieranie danych...</td></tr>
) : sortedPages.map(page => (
<tr key={page.id} className="border-t border-white/5 hover:bg-white/[0.02] transition-colors group">
<td className="p-4"><span className={`px-2 py-0.5 rounded text-[10px] font-black ${page.status===200 ? 'bg-emerald-500/10 text-emerald-400' : 'bg-red-500/10 text-red-400'}`}>{page.status || 'ERROR'}</span></td>
<td className="p-4 font-black text-[10px] text-slate-500 uppercase">{page.lang || '??'}</td>
<td className="p-4 max-w-sm"><div className="truncate font-medium"><a href={page.url} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300 transition-colors underline decoration-blue-400/30 hover:decoration-blue-400 underline-offset-4">{page.url}</a></div><div className="text-[10px] text-slate-500 mt-0.5 truncate italic">Źródło: {page.source_url ? <a href={page.source_url} target="_blank" rel="noopener noreferrer" className="hover:text-slate-300 transition-colors">{page.source_url}</a> : 'Bezpośrednie'}</div></td>
<td className="p-4 text-slate-400 tabular-nums">{page.total_time?.toFixed(3)}s</td>
<td className="p-4">
{page.schema_critical > 0 ? <span className="text-red-500 text-[10px] font-black uppercase">Krytyczny</span> :
page.schema_warnings > 0 ? <span className="text-amber-500 text-[10px] font-black uppercase">Ostrzeżenia</span> : <span className="text-emerald-500 text-[10px] font-black uppercase">OK</span>}
</td>
<td className="p-4 text-right"><button onClick={() => viewAnalysis(page)} className="bg-blue-500/10 hover:bg-blue-600 text-blue-400 hover:text-white px-3 py-1.5 rounded-lg text-[10px] font-black transition-all uppercase">Analiza</button></td>
</tr>
))}
</tbody>
</table>
</div>
</div>
</div>
) : (
<div className="glass rounded-3xl border border-white/5 shadow-2xl animate-fade-in">
<div className="overflow-auto max-h-[75vh]">
<table className="w-full text-left border-collapse">
<thead className="bg-slate-900 text-[10px] font-black text-slate-400 uppercase tracking-widest sticky top-0 z-10 shadow-md">
<tr>
<th className="p-5 w-40">SKU</th>
<th className="p-5 w-40">POLE</th>
{translations?.langs?.map(l => <th key={l} className="p-5 text-center">{l.toUpperCase()}</th>)}
</tr>
</thead>
<tbody className="text-sm">
{loading ? (
<tr><td colSpan={2 + (translations?.langs?.length || 0)} className="p-20 text-center text-slate-600 font-bold animate-pulse uppercase tracking-[0.2em]">Analiza tłumaczeń...</td></tr>
) : translations?.data?.length > 0 ? translations.data.map((t, idx) => {
const isFirstOfSku = idx === 0 || translations.data[idx-1].sku !== t.sku;
return (
<tr key={idx} className={`hover:bg-white/[0.02] transition-colors ${isFirstOfSku ? 'border-t-2 border-t-white/10' : 'border-t border-white/5 border-dashed'}`}>
<td className="p-5 font-bold tabular-nums">
{isFirstOfSku ? <a href={t.url} target="_blank" rel="noopener noreferrer" className="text-blue-400 hover:text-blue-300 transition-colors underline decoration-blue-400/30 hover:decoration-blue-400 underline-offset-4">{t.sku}</a> : null}
</td>
<td className="p-5 uppercase text-[10px] font-black text-slate-400">{t.field}</td>
{translations.langs.map(l => (
<td key={l} className="p-5 text-center">
{t[l] === 'V' ? <i data-lucide="check-circle" className="w-4 h-4 text-emerald-500/70 mx-auto"></i> : <i data-lucide="x-circle" className="w-5 h-5 text-red-500 mx-auto drop-shadow-md"></i>}
</td>
))}
</tr>
);
}) : (
<tr><td colSpan={10} className="p-20 text-center text-slate-600 font-bold uppercase tracking-widest">Brak danych o tłumaczeniach.</td></tr>
)}
</tbody>
</table>
</div>
</div>
)}
{selectedPage && (
<div className="fixed inset-0 bg-slate-950/90 backdrop-blur-md flex items-center justify-center p-4 z-50 animate-fade-in">
<div className="glass w-full max-w-6xl max-h-[90vh] overflow-hidden flex flex-col rounded-[2.5rem] border border-white/10 shadow-2xl">
<div className="p-6 border-b border-white/5 flex justify-between items-center bg-slate-900/50">
<div><p className="text-blue-400 text-[10px] font-black uppercase tracking-widest mb-1">Pełny audyt strony</p><h2 className="text-lg font-bold text-slate-100 truncate max-w-2xl">{selectedPage.url}</h2></div>
<button onClick={() => setSelectedPage(null)} className="p-2 hover:bg-white/10 rounded-xl transition-colors"><i data-lucide="x"></i></button>
</div>
<div className="flex bg-slate-900/50 border-b border-white/5">
<button onClick={()=>setActiveTab('schema')} className={`px-8 py-3 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab==='schema'?'text-blue-400 border-b-2 border-blue-400 bg-blue-400/5':'text-slate-500 hover:text-slate-300'}`}>Schema.org</button>
<button onClick={()=>setActiveTab('metadata')} className={`px-8 py-3 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab==='metadata'?'text-purple-400 border-b-2 border-purple-400 bg-purple-400/5':'text-slate-500 hover:text-slate-300'}`}>Metadane SEO</button>
<button onClick={()=>setActiveTab('images')} className={`px-8 py-3 text-[10px] font-black uppercase tracking-widest transition-all ${activeTab==='images'?'text-emerald-400 border-b-2 border-emerald-400 bg-emerald-400/5':'text-slate-500 hover:text-slate-300'}`}>Audyt Grafiki</button>
</div>
<div className="p-6 overflow-y-auto">
{activeTab === 'schema' ? (
<div>
<div className="mb-10">
<h3 className="text-slate-500 text-[10px] font-black mb-4 uppercase tracking-[0.2em] border-b border-white/5 pb-2">I. Audyt pól Schema.org</h3>
{analysisData.schemas.length > 0 ? analysisData.schemas.map((s, i) => {
const fields = SCHEMA_DEFS[s.type] || [];
return (
<div key={i} className="mb-6 glass rounded-2xl overflow-hidden border border-white/5">
<div className="px-4 py-2 bg-white/5 flex items-center space-x-2 text-[10px] font-black uppercase text-slate-400"><i data-lucide="file-json" className="w-3 h-3"></i><span>{s.type}</span></div>
<table className="w-full text-[10px] border-collapse">
<tbody>
{fields.map((f, fi) => {
const val = getVal(s.data, f.field);
const ex = val !== undefined && val !== null && val !== '';
return (
<tr key={fi} className="border-t border-white/5">
<td className="p-2.5 text-slate-400 w-1/3">{f.label}</td>
<td className="p-2.5 text-slate-200 truncate max-w-[200px] font-medium">{ex ? (typeof val==='object'?'Obiekt':String(val)):''}</td>
<td className="p-2.5 text-right font-black uppercase">{ex ? <span className="text-emerald-500">OK</span> : f.status==='req' ? <span className="text-red-500">Wymagane</span> : f.status==='warn' ? <span className="text-amber-500">Zalecane</span> : <span className="text-slate-600">Opcjonalne</span>}</td>
</tr>
);
})}
</tbody>
</table>
</div>
);
}) : <p className="text-center py-10 text-slate-600 font-bold uppercase tracking-widest text-[10px]">Brak danych strukturalnych</p>}
</div>
<div>
<h3 className="text-slate-500 text-[10px] font-black mb-4 uppercase tracking-[0.2em] border-b border-white/5 pb-2">II. Kod JSON-LD</h3>
{analysisData.schemas.map((s, i) => (
<pre key={i} className="bg-slate-950/80 p-5 rounded-xl overflow-x-auto text-[11px] text-blue-200/70 border border-white/5 font-mono mb-4 last:mb-0">{JSON.stringify(s.data, null, 2)}</pre>
))}
</div>
</div>
) : activeTab === 'metadata' ? (
<div>
<h3 className="text-slate-500 text-[10px] font-black mb-4 uppercase tracking-[0.2em] border-b border-white/5 pb-2">Metadane SEO</h3>
<div className="glass rounded-2xl overflow-hidden border border-white/5 p-5 text-sm text-slate-300">
<div className="mb-4">
<span className="block text-[10px] font-black uppercase text-slate-500 mb-1">Tag Title</span>
<div className="font-bold text-slate-100 bg-slate-900/50 p-3 rounded-xl border border-white/5">{selectedPage.title || 'Brak tagu <title>'}</div>
</div>
<div className="mb-4">
<span className="block text-[10px] font-black uppercase text-slate-500 mb-1">Meta Description</span>
<div className="font-medium text-slate-300 bg-slate-900/50 p-3 rounded-xl border border-white/5">{selectedPage.meta_desc || 'Brak description'}</div>
</div>
<div>
<span className="block text-[10px] font-black uppercase text-slate-500 mb-1">Link Canonical</span>
<div className="font-mono text-xs text-blue-400 bg-slate-900/50 p-3 rounded-xl border border-white/5 truncate">{selectedPage.canonical || 'Brak canonical'}</div>
{selectedPage.canonical && selectedPage.canonical !== selectedPage.url && <div className="mt-2 text-[10px] text-amber-500 font-bold uppercase"><i data-lucide="alert-triangle" className="w-3 h-3 inline mr-1"></i>Canonical wskazuje na inną stronę!</div>}
{selectedPage.canonical && selectedPage.canonical === selectedPage.url && <div className="mt-2 text-[10px] text-emerald-500 font-bold uppercase"><i data-lucide="check-circle" className="w-3 h-3 inline mr-1"></i>Samoodwołujący (Zgodny z URL)</div>}
</div>
</div>
</div>
) : (
<div>
<h3 className="text-slate-500 text-[10px] font-black mb-4 uppercase tracking-[0.2em] border-b border-white/5 pb-2">Audyt optymalizacji obrazów</h3>
<div className="glass rounded-2xl overflow-hidden border border-white/5">
<table className="w-full text-left border-collapse">
<thead className="bg-white/5 text-[9px] font-black text-slate-500 uppercase tracking-widest">
<tr><th className="p-3">Podgląd</th><th className="p-3">Atrybut ALT</th><th className="p-3">Format Modern</th><th className="p-3 text-right">Status</th></tr>
</thead>
<tbody className="text-[10px]">
{analysisData.images.length > 0 ? analysisData.images.map((img, i) => (
<tr key={i} className="border-t border-white/5 hover:bg-white/[0.02]">
<td className="p-3"><img src={img.img_url} className="w-10 h-10 object-cover rounded bg-slate-800" onError={(e)=>e.target.src='https://via.placeholder.com/40'} /></td>
<td className="p-3"><span className={img.alt==='[BRAK]'?'text-red-400 font-bold':'text-slate-300'}>{img.alt}</span></td>
<td className="p-3">{img.is_modern ? <span className="text-emerald-400 font-bold">TAK (Bezpośrednio)</span> : img.has_modern_source ? <span className="text-blue-400 font-bold">TAK (Picture/Srcset)</span> : <span className="text-amber-500 font-bold">NIE (Stary format)</span>}</td>
<td className="p-3 text-right">{img.alt!=='[BRAK]' && (img.is_modern || img.has_modern_source) ? <span className="text-emerald-500 font-black">ZOPTYMALIZOWANO</span> : <span className="text-amber-500 font-black text-[9px]">DO POPRAWY</span>}</td>
</tr>
)) : <tr><td colSpan="4" className="p-10 text-center text-slate-600 font-bold uppercase">Nie znaleziono obrazów</td></tr>}
</tbody>
</table>
</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>
</body>
</html>
"""
@app.get("/api/list-dbs")
def list_dbs():
dbs = glob.glob("scans/*.db")
# Zwracamy same nazwy plików dla ładniejszego widoku w select
return sorted([os.path.basename(db) for db in dbs], reverse=True)
@app.get("/api/stats")
def get_stats(db: str):
conn = get_db_conn(db)
cursor = conn.cursor()
stats = {"total_pages": 0, "errors": 0, "avg_time": 0, "schema_objects": 0, "img_issues": 0, "translation_errors": 0}
try:
stats["total_pages"] = cursor.execute("SELECT COUNT(*) FROM pages").fetchone()[0]
stats["errors"] = cursor.execute("SELECT COUNT(*) FROM pages WHERE status != 200 AND status != 0").fetchone()[0]
stats["avg_time"] = cursor.execute("SELECT AVG(total_time) FROM pages WHERE total_time > 0").fetchone()[0] or 0
stats["schema_objects"] = cursor.execute("SELECT COUNT(*) FROM structured_data").fetchone()[0]
except: pass
try:
img_stats = cursor.execute("SELECT SUM(images_no_alt), SUM(images_no_webp) FROM pages").fetchone()
stats["img_issues"] = (img_stats[0] or 0) + (img_stats[1] or 0)
except: pass
try:
stats["translation_errors"] = cursor.execute("SELECT COUNT(*) FROM translation_audit").fetchone()[0]
except: pass
conn.close()
return stats
@app.get("/api/pages")
def get_pages(db: str, status_type: Optional[str] = "all"):
conn = get_db_conn(db)
cursor = conn.cursor()
try:
query = "SELECT * FROM pages"
try:
cursor.execute("SELECT images_no_alt FROM pages LIMIT 1")
has_img_cols = True
except: has_img_cols = False
if status_type == "error": query += " WHERE status != 200 AND status != 0"
elif status_type == "noindex": query += " WHERE index_status LIKE 'Noindex%'"
elif status_type == "slow": query += " WHERE total_time > 1.5"
elif status_type == "images" and has_img_cols: query += " WHERE images_no_alt > 0 OR images_no_webp > 0"
query += " ORDER BY id DESC LIMIT 1000"
pages = cursor.execute(query).fetchall()
return [dict(p) for p in pages]
except: return []
finally: conn.close()
@app.get("/api/translations")
def get_translations(db: str):
conn = get_db_conn(db)
cursor = conn.cursor()
try:
try:
cursor.execute("SELECT title, meta_desc FROM pages LIMIT 1")
has_meta = True
except: has_meta = False
if has_meta:
query = """
SELECT s.sku, p.lang, s.full_json, MIN(p.url) as url, MAX(p.title) as title, MAX(p.meta_desc) as meta_desc
FROM structured_data s
JOIN pages p ON s.page_id = p.id
WHERE s.sku IS NOT NULL AND s.sku != 'None' AND s.sku != '' AND s.schema_type LIKE '%Product%'
GROUP BY s.sku, p.lang
"""
else:
query = """
SELECT s.sku, p.lang, s.full_json, MIN(p.url) as url, '' as title, '' as meta_desc
FROM structured_data s
JOIN pages p ON s.page_id = p.id
WHERE s.sku IS NOT NULL AND s.sku != 'None' AND s.sku != '' AND s.schema_type LIKE '%Product%'
GROUP BY s.sku, p.lang
"""
rows = cursor.execute(query).fetchall()
sku_map = {}
langs_set = set()
for r in rows:
sku = str(r['sku']).strip()
lang = str(r['lang']).lower().strip()
if '-' in lang: lang = lang.split('-')[0]
langs_set.add(lang)
try: data = json.loads(r['full_json'])
except: continue
obj = {}
if isinstance(data, list): obj = next((item for item in data if 'Product' in str(item.get('@type', ''))), {})
else: obj = data if 'Product' in str(data.get('@type', '')) else {}
name = obj.get('name', '').strip()
desc = obj.get('description', '').strip()
title = (r['title'] or '').strip()
meta_desc = (r['meta_desc'] or '').strip()
slug = ''
if r['url']:
parts = r['url'].rstrip('/').split('/')
if parts: slug = parts[-1].split('?')[0].split('#')[0]
if sku not in sku_map: sku_map[sku] = {'langs': {}, 'url': r['url']}
sku_map[sku]['langs'][lang] = {
'nazwa': name, 'opis': desc,
'nazwa seo': title, 'opis seo': meta_desc, 'slug': slug
}
all_langs = sorted(list(langs_set))
if 'pl' in all_langs: all_langs.remove('pl')
results = []
fields = ['nazwa', 'opis', 'nazwa seo', 'opis seo', 'slug']
for sku, info in sku_map.items():
if 'pl' not in info['langs']: continue
pl_data = info['langs']['pl']
sku_has_errors = False
sku_rows = []
for field in fields:
pl_val = pl_data.get(field, '')
if not pl_val: continue
row = {'sku': sku, 'field': field, 'url': info['url']}
for lang in all_langs:
l_val = info['langs'].get(lang, {}).get(field, '')
if not l_val or l_val == pl_val:
row[lang] = 'X'
sku_has_errors = True
else:
row[lang] = 'V'
sku_rows.append(row)
if sku_has_errors:
results.extend(sku_rows)
return {"langs": all_langs, "data": results}
except Exception as e:
print(f"Error in translations: {e}")
return {"langs": [], "data": []}
finally: conn.close()
@app.get("/api/analysis/{page_id}")
def get_analysis(db: str, page_id: int):
conn = get_db_conn(db)
cursor = conn.cursor()
try:
schemas = cursor.execute("SELECT schema_type, full_json FROM structured_data WHERE page_id = ?", (page_id,)).fetchall()
try: images = cursor.execute("SELECT img_url, alt, is_modern, has_modern_source FROM images_audit WHERE page_id = ?", (page_id,)).fetchall()
except: images = []
schema_list = []
for s in schemas:
try: schema_list.append({"type": s["schema_type"], "data": json.loads(s["full_json"])})
except: schema_list.append({"type": s["schema_type"], "data": s["full_json"]})
return {"schemas": schema_list, "images": [dict(img) for img in images]}
finally:
conn.close()
@app.get("/api/export-csv")
def export_csv(db: str, status_type: Optional[str] = "all"):
conn = get_db_conn(db)
cursor = conn.cursor()
try:
cursor.execute("SELECT images_no_alt FROM pages LIMIT 1")
has_img_cols = True
except: has_img_cols = False
query = "SELECT * FROM pages"
if status_type == "error": query += " WHERE status != 200 AND status != 0"
elif status_type == "noindex": query += " WHERE index_status LIKE 'Noindex%'"
elif status_type == "slow": query += " WHERE total_time > 1.5"
elif status_type == "images" and has_img_cols: query += " WHERE images_no_alt > 0 OR images_no_webp > 0"
query += " ORDER BY id DESC"
pages = cursor.execute(query).fetchall()
conn.close()
output = io.StringIO()
writer = csv.writer(output, delimiter=';')
if pages:
keys = list(dict(pages[0]).keys())
writer.writerow([k.upper() for k in keys])
for p in pages:
writer.writerow([dict(p).get(k, '') for k in keys])
else:
writer.writerow(['BRAK DANYCH'])
output.seek(0)
filename = f"raport_seo_{status_type}_{db.replace('.db', '')}.csv"
return StreamingResponse(io.BytesIO(output.getvalue().encode('utf-8-sig')), media_type="text/csv", headers={"Content-Disposition": f"attachment; filename={filename}"})
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8000)