122 lines
7.8 KiB
Python
122 lines
7.8 KiB
Python
#!/usr/bin/env python3
|
|
"""Anpassningsbar widget-dashboard för Scrape Console (sidan /dash).
|
|
Layouten sparas i webbläsarens localStorage. Widgets läser befintliga endpoints."""
|
|
|
|
DASH_PAGE = """<!DOCTYPE html><html lang="sv"><head><meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
<title>Scrape Console · Min dashboard</title>
|
|
<style>body{background:#0d1117;color:#e6edf3;font-family:system-ui,sans-serif;margin:0;padding:16px}
|
|
a{color:#58a6ff}h1{font-size:20px;margin:0 0 8px}
|
|
#toolbar{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:12px;align-items:center;background:#161b22;border:1px solid #30363d;border-radius:8px;padding:10px}
|
|
select,input{background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:6px 10px;font-size:13px}
|
|
button{background:#21262d;color:#e6edf3;border:1px solid #30363d;border-radius:6px;padding:6px 12px;cursor:pointer;font-size:13px}
|
|
button:hover{background:#30363d}
|
|
#grid{display:grid;grid-template-columns:repeat(3,1fr);gap:12px}
|
|
.box{background:#161b22;border:1px solid #30363d;border-radius:8px;padding:12px;min-width:0}
|
|
.box.w1{grid-column:span 1}.box.w2{grid-column:span 2}.box.w3{grid-column:span 3}
|
|
.box h3{font-size:14px;margin:0 0 6px}
|
|
.tools{float:right;display:flex;gap:2px}.tools button{padding:1px 7px;font-size:11px;margin:0}
|
|
.bar{background:#30363d;border-radius:4px;height:8px;margin:4px 0 8px}.bar>i{display:block;height:8px;border-radius:4px;background:#1f6feb}
|
|
.src{font-size:12px;margin:2px 0}.src b{color:#e6edf3}.kv{font-size:12px;color:#8b949e}.kv b{color:#e6edf3}
|
|
pre{background:#0d1117;border:1px solid #30363d;border-radius:6px;padding:8px;font-size:11px;overflow:auto;max-height:220px;white-space:pre-wrap}
|
|
table{border-collapse:collapse;width:100%;font-size:11px}th,td{border:1px solid #30363d;padding:4px 6px;text-align:left;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
th{background:#0d1117}svg{width:100%;height:50px}
|
|
textarea{width:100%;background:#0d1117;color:#e6edf3;border:1px solid #30363d;border-radius:6px;font-size:12px;min-height:60px}
|
|
@media(max-width:900px){#grid{grid-template-columns:1fr}.box.w1,.box.w2,.box.w3{grid-column:span 1}}</style></head>
|
|
<body><h1>🕷️ <a href="/">Scrape Console</a> · <a href="/data-page">Alla data</a> · Min dashboard</h1>
|
|
<div id="toolbar"><b>Lägg till box:</b><select id="wtype"></select><select id="wref"></select>
|
|
<button onclick="addBox()">+ Lägg till</button><button onclick="resetDash()">↺ Återställ</button>
|
|
<span id="ts" style="color:#8b949e;font-size:12px"></span></div>
|
|
<div id="grid"></div>
|
|
<script>
|
|
const WDEF = {
|
|
progress: {t:'Progress per källa', refs:['*']},
|
|
rate: {t:'Takt-graf', refs:['biluppgifter','fordonskollen','fordonssok','carinfo']},
|
|
log: {t:'Loggsvans', refs:['multi:SF multi','carinfo:car.info']},
|
|
dataset: {t:'Datasett-tabell', refs:[]},
|
|
status: {t:'Scraper-status', refs:['*']},
|
|
ckpt: {t:'Checkpoint', refs:['*']},
|
|
note: {t:'Anteckning (egen text)', refs:['*']},
|
|
};
|
|
let L = JSON.parse(localStorage.getItem('scrape-dash') || 'null') || [
|
|
{id:1,type:'status',ref:'*',w:3},
|
|
{id:2,type:'progress',ref:'*',w:2},
|
|
{id:3,type:'rate',ref:'fordonssok',w:1},
|
|
{id:4,type:'log',ref:'multi',w:2},
|
|
{id:5,type:'dataset',ref:'vehicles_clean',w:3},
|
|
];
|
|
let DS = [], ST = null, HIST = [];
|
|
function save(){localStorage.setItem('scrape-dash', JSON.stringify(L));}
|
|
function resetDash(){localStorage.removeItem('scrape-dash');location.reload();}
|
|
async function init(){
|
|
DS = await (await fetch('/datasets')).json();
|
|
WDEF.dataset.refs = DS.map(d=>d.id+':'+d.label.split('·').pop().trim().slice(0,28));
|
|
const t = document.getElementById('wtype'), r = document.getElementById('wref');
|
|
t.innerHTML = Object.entries(WDEF).map(([k,v])=>`<option value="${k}">${v.t}</option>`).join('');
|
|
t.onchange = ()=>{r.innerHTML = WDEF[t.value].refs.map(x=>{const [v,l]=x.split(/:(.+)/);return `<option value="${v}">${l||v}</option>`;}).join('');};
|
|
t.onchange(); render();
|
|
}
|
|
function addBox(){
|
|
const type = document.getElementById('wtype').value, ref = document.getElementById('wref').value;
|
|
let extra = {};
|
|
if(type==='note'){const txt = prompt('Text:',''); if(txt===null) return; extra={text:txt};}
|
|
L.push({id:Date.now(), type, ref, w:type==='status'||type==='dataset'?3:1, ...extra});
|
|
save(); render();
|
|
}
|
|
function boxOp(id, op){
|
|
const i = L.findIndex(b=>b.id===id); if(i<0) return;
|
|
if(op==='del') L.splice(i,1);
|
|
if(op==='left' && i>0) [L[i-1],L[i]] = [L[i],L[i-1]];
|
|
if(op==='right' && i<L.length-1) [L[i+1],L[i]] = [L[i],L[i+1]];
|
|
if(op==='w') L[i].w = L[i].w%3+1;
|
|
if(op==='edit' && L[i].type==='note'){const t=prompt('Text:',L[i].text||''); if(t!==null)L[i].text=t;}
|
|
save(); render();
|
|
}
|
|
function tools(b){
|
|
return `<span class="tools"><button onclick="boxOp(${b.id},'left')" title="flytta vänster">◀</button><button onclick="boxOp(${b.id},'right')" title="flytta höger">▶</button><button onclick="boxOp(${b.id},'w')" title="bredd">↔${b.w}</button>${b.type==='note'?`<button onclick="boxOp(${b.id},'edit')" title="redigera">✎</button>`:''}<button onclick="boxOp(${b.id},'del')" title="ta bort">✕</button></span>`;
|
|
}
|
|
async function render(){
|
|
ST = await (await fetch('/state')).json();
|
|
HIST = await (await fetch('/history')).json();
|
|
document.getElementById('ts').textContent = 'Uppdaterad ' + ST.ts + ' (auto 15 s)';
|
|
const g = document.getElementById('grid'); g.innerHTML = '';
|
|
for(const b of L){
|
|
let h = `<div class="box w${b.w}">${tools(b)}`;
|
|
if(b.type==='status'){
|
|
h += `<h3>Scraper-status</h3>` + ST.scrapers.map(c=>`<div class="src"><b>${c.label}</b> ${c.active} · ${c.mem_mb} MB · ${c.log?'':''}</div>`).join('');
|
|
} else if(b.type==='progress'){
|
|
const p = ST.scrapers[0].progress || {};
|
|
h += `<h3>Progress per källa</h3>`;
|
|
for(const [k,v] of Object.entries(p)){ if(k.startsWith('_')) continue;
|
|
const pct = 100*v.pos/v.total;
|
|
h += `<div class="src"><b>${k}</b> ${v.ok} ok/${v.fail} fail · ${(pct).toFixed(2)}%</div><div class="bar"><i style="width:${Math.min(100,pct)}%"></i></div>`; }
|
|
} else if(b.type==='rate'){
|
|
h += `<h3>Takt: ${b.ref}</h3>` + spark(b.ref);
|
|
} else if(b.type==='log'){
|
|
const [src] = b.ref.split(':');
|
|
const t = await (await fetch(`/log?src=${src}&n=12`)).text();
|
|
h += `<h3>Logg: ${b.ref}</h3><pre>${t.replace(/</g,'<')}</pre>`;
|
|
} else if(b.type==='dataset'){
|
|
const d = await (await fetch(`/data?ds=${b.ref}&page=1`)).json();
|
|
const cols = (d.cols||[]).slice(0,5);
|
|
h += `<h3>${(DS.find(x=>x.id===b.ref)||{}).label||b.ref} (${d.total})</h3><table><tr>${cols.map(c=>`<th>${c}</th>`).join('')}</tr>` +
|
|
(d.rows||[]).slice(0,8).map(r=>`<tr>${cols.map(c=>`<td>${String(r[c]??'').replace(/</g,'<')}</td>`).join('')}</tr>`).join('') + `</table>`;
|
|
} else if(b.type==='ckpt'){
|
|
h += `<h3>car.info checkpoint</h3><div class="src"><b>${Object.entries(ST.scrapers[1].ckpt||{}).map(([k,v])=>k+'='+v).join(' · ')}</b></div>`;
|
|
} else if(b.type==='note'){
|
|
h += `<h3>📝 Anteckning</h3><div class="src">${(b.text||'').replace(/</g,'<')}</div>`;
|
|
}
|
|
g.innerHTML += h + '</div>';
|
|
}
|
|
}
|
|
function spark(src){
|
|
const pts = HIST.filter(h=>h.ok && h.ok[src]!==undefined).slice(-60);
|
|
if(pts.length < 2) return '<div class="kv">ingen historik ännu</div>';
|
|
const vals = pts.map(h=>h.ok[src]), mx = Math.max(...vals), mn = Math.min(...vals);
|
|
const W=300,H=46, path = vals.map((v,i)=>`${(i/(vals.length-1)*W).toFixed(1)},${(H-2-((v-mn)/Math.max(1,mx-mn))*(H-4)).toFixed(1)}`).join(' ');
|
|
const rate = ((vals[vals.length-1]-vals[0])/Math.max(1,(pts[pts.length-1].t-pts[0].t)/60)).toFixed(0);
|
|
return `<svg viewBox="0 0 ${W} ${H}"><polyline points="${path}" fill="none" stroke="#1f6feb" stroke-width="1.5"/></svg><div class="kv"><b>${rate}/min</b></div>`;
|
|
}
|
|
init(); setInterval(render, 15000);
|
|
</script></body></html>"""
|