Админка: активность по дням (заходы/документы сайт+бот/страницы), подписки — вниз
- daily: split файлов по источнику (files_site/files_bot по user_id==WEB_USER_ID) - Секция «Активность по дням» — 3 малых кратных (визуально выровнены по дню): заходы (синий), документы (стек: сайт #007AFF / бот #5856d6), страницы (аква #1baf7a); разные шкалы у метрик, поэтому small multiples, не один график с двумя осями - «Активные подписки» перенесены в самый низ страницы Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
102
app/main.py
102
app/main.py
@@ -2988,11 +2988,19 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
.pill.cancel{ background:rgba(255,59,48,.14); color:#c1281e; }
|
.pill.cancel{ background:rgba(255,59,48,.14); color:#c1281e; }
|
||||||
.pill.web{ background:rgba(0,122,255,.12); color:#0060df; }
|
.pill.web{ background:rgba(0,122,255,.12); color:#0060df; }
|
||||||
.pill.bot{ background:rgba(88,86,214,.14); color:#4b48c4; }
|
.pill.bot{ background:rgba(88,86,214,.14); color:#4b48c4; }
|
||||||
.bars{ display:flex; align-items:flex-end; gap:5px; height:90px; }
|
.bars{ display:flex; align-items:flex-end; gap:5px; height:78px; }
|
||||||
.bars .b{ flex:1; background:var(--blue); border-radius:4px 4px 0 0; min-height:2px; position:relative; opacity:.85; }
|
.bars .b{ flex:1; background:var(--blue); border-radius:4px 4px 0 0; min-height:2px; position:relative; opacity:.9; }
|
||||||
.bars .b span{ position:absolute; bottom:-18px; left:0; right:0; text-align:center; font-size:10px; color:var(--sec); }
|
.bars .b span{ position:absolute; bottom:-18px; left:0; right:0; text-align:center; font-size:10px; color:var(--sec); }
|
||||||
.bars .b b{ position:absolute; top:-15px; left:0; right:0; text-align:center; font-size:10px; font-weight:600; color:var(--ink); }
|
.bars .b b{ position:absolute; top:-15px; left:0; right:0; text-align:center; font-size:10px; font-weight:600; color:var(--ink); }
|
||||||
.bars{ margin-top:16px; }
|
.bars{ margin-top:16px; }
|
||||||
|
/* Малые кратные: заходы / документы (сайт+бот) / страницы */
|
||||||
|
.daychart{ margin-top:12px; padding-bottom:24px; }
|
||||||
|
.daychart-cap{ font-size:12px; color:var(--ink); margin-bottom:20px; display:flex; align-items:center; gap:10px; flex-wrap:wrap; }
|
||||||
|
.dc-leg{ display:inline-flex; align-items:center; gap:4px; }
|
||||||
|
.dc-leg i{ width:9px; height:9px; border-radius:2px; display:inline-block; }
|
||||||
|
.b.stack{ background:transparent; display:flex; flex-direction:column; overflow:hidden; opacity:1; }
|
||||||
|
.b.stack .seg{ width:100%; flex:none; }
|
||||||
|
.b.stack .seg.top{ border-bottom:1.5px solid var(--card); }
|
||||||
.muted{ color:var(--sec); }
|
.muted{ color:var(--sec); }
|
||||||
#gate{ max-width:360px; margin:80px auto; background:#fff; border-radius:16px; padding:24px; text-align:center; }
|
#gate{ max-width:360px; margin:80px auto; background:#fff; border-radius:16px; padding:24px; text-align:center; }
|
||||||
#gate input{ width:100%; padding:11px 12px; border:1px solid var(--line); border-radius:10px; font:inherit; margin:14px 0; }
|
#gate input{ width:100%; padding:11px 12px; border:1px solid var(--line); border-radius:10px; font:inherit; margin:14px 0; }
|
||||||
@@ -3028,14 +3036,23 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>Активные подписки</h2>
|
<h2>Активность по дням <span class="muted">· 14 дней</span></h2>
|
||||||
<div class="scroll"><table><thead><tr><th>Аккаунт</th><th>Активна до</th><th class="num">Осталось</th></tr></thead>
|
<div class="daychart">
|
||||||
<tbody id="t-subs"></tbody></table></div>
|
<div class="daychart-cap">Заходы на сайт <span class="muted" id="vis-total"></span></div>
|
||||||
</section>
|
<div class="bars" id="bars-visits"></div>
|
||||||
|
</div>
|
||||||
<section>
|
<div class="daychart">
|
||||||
<h2>Заходы на сайт <span class="muted" id="vis-total"></span></h2>
|
<div class="daychart-cap">Обработано документов
|
||||||
<div class="bars" id="bars"></div>
|
<span class="dc-leg"><i style="background:#007AFF"></i>сайт</span>
|
||||||
|
<span class="dc-leg"><i style="background:#5856d6"></i>бот</span>
|
||||||
|
<span class="muted" id="docs-cap"></span>
|
||||||
|
</div>
|
||||||
|
<div class="bars" id="bars-docs"></div>
|
||||||
|
</div>
|
||||||
|
<div class="daychart">
|
||||||
|
<div class="daychart-cap">Обработано страниц <span class="muted" id="pages-cap"></span></div>
|
||||||
|
<div class="bars" id="bars-pages"></div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -3076,6 +3093,12 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
<div class="scroll"><table><thead><tr><th>Дата</th><th>Файл</th><th class="num">Стр.</th><th class="num">Время</th><th class="num">Исходник</th><th class="num">Результат</th><th>Кач-во</th><th>Сшивка</th><th>OCR</th><th>Источник</th></tr></thead>
|
<div class="scroll"><table><thead><tr><th>Дата</th><th>Файл</th><th class="num">Стр.</th><th class="num">Время</th><th class="num">Исходник</th><th class="num">Результат</th><th>Кач-во</th><th>Сшивка</th><th>OCR</th><th>Источник</th></tr></thead>
|
||||||
<tbody id="t-docs"></tbody></table></div>
|
<tbody id="t-docs"></tbody></table></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<h2>Активные подписки</h2>
|
||||||
|
<div class="scroll"><table><thead><tr><th>Аккаунт</th><th>Активна до</th><th class="num">Осталось</th></tr></thead>
|
||||||
|
<tbody id="t-subs"></tbody></table></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -3126,17 +3149,45 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
return '<tr><td>'+esc(s.account_id)+'</td><td>'+dOnly(s.sub_until)+'</td><td class="num">'+daysLeft(s.sub_until)+'</td></tr>';
|
return '<tr><td>'+esc(s.account_id)+'</td><td>'+dOnly(s.sub_until)+'</td><td class="num">'+daysLeft(s.sub_until)+'</td></tr>';
|
||||||
}).join('') : '<tr><td colspan="3" class="muted">Нет активных подписок</td></tr>';
|
}).join('') : '<tr><td colspan="3" class="muted">Нет активных подписок</td></tr>';
|
||||||
|
|
||||||
// Заходы (мини-бары)
|
// Активность по дням — три малых кратных (заходы / документы сайт+бот / страницы)
|
||||||
var days = (d.visits.days||[]).slice(0,14).reverse();
|
var dl = d.daily || []; // 14 дней, старые слева
|
||||||
var mx = Math.max(1, Math.max.apply(null, days.map(function(x){ return x.count; }).concat([1])));
|
function dd(x){ return x.day.slice(8)+'.'+x.day.slice(5,7); }
|
||||||
document.getElementById('bars').innerHTML = days.length ? days.map(function(x){
|
function barMax(fn){ var m=1; dl.forEach(function(x){ var v=fn(x)||0; if(v>m) m=v; }); return m; }
|
||||||
var h = Math.round(x.count/mx*100);
|
function kfmt(n){ n=n||0; return n>9999 ? (Math.round(n/100)/10)+'k' : n.toLocaleString('ru-RU'); }
|
||||||
var dd = x.day.slice(8)+'.'+x.day.slice(5,7);
|
var emptyBars = '<div class="muted">Пока нет данных</div>';
|
||||||
return '<div class="b" style="height:'+h+'%" title="'+x.day+': '+x.count+' заходов, уник. '+(x.uniques||0)+'"><b>'+x.count+'</b><span>'+dd+'</span></div>';
|
|
||||||
}).join('') : '<div class="muted">Пока нет данных</div>';
|
// 1) Заходы
|
||||||
|
var mV = barMax(function(x){return x.visits;});
|
||||||
|
document.getElementById('bars-visits').innerHTML = dl.length ? dl.map(function(x){
|
||||||
|
var h=Math.round((x.visits||0)/mV*100);
|
||||||
|
return '<div class="b" style="height:'+h+'%" title="'+x.day+': '+(x.visits||0)+' заходов, уник. '+(x.uniques||0)+'"><b>'+(x.visits||0)+'</b><span>'+dd(x)+'</span></div>';
|
||||||
|
}).join('') : emptyBars;
|
||||||
document.getElementById('vis-total').textContent = '· всего ' + (d.visits.total||0).toLocaleString('ru-RU') +
|
document.getElementById('vis-total').textContent = '· всего ' + (d.visits.total||0).toLocaleString('ru-RU') +
|
||||||
' (уник. ' + (d.visits.unique_total||0).toLocaleString('ru-RU') + ')';
|
' (уник. ' + (d.visits.unique_total||0).toLocaleString('ru-RU') + ')';
|
||||||
|
|
||||||
|
// 2) Документы — стек сайт (низ, синий) + бот (верх, фиолетовый)
|
||||||
|
var mD = barMax(function(x){return (x.files_site||0)+(x.files_bot||0);});
|
||||||
|
var totSite=0, totBot=0;
|
||||||
|
document.getElementById('bars-docs').innerHTML = dl.length ? dl.map(function(x){
|
||||||
|
var si=x.files_site||0, bo=x.files_bot||0, tot=si+bo; totSite+=si; totBot+=bo;
|
||||||
|
var h=Math.round(tot/mD*100);
|
||||||
|
var both = si>0 && bo>0, segs='';
|
||||||
|
if(bo>0) segs += '<div class="seg'+(both?' top':'')+'" style="height:'+Math.round(bo/tot*100)+'%;background:#5856d6"></div>';
|
||||||
|
if(si>0) segs += '<div class="seg" style="height:'+Math.round(si/tot*100)+'%;background:#007AFF"></div>';
|
||||||
|
return '<div class="b stack" style="height:'+(tot?h:0)+'%" title="'+x.day+': '+tot+' документов (сайт '+si+', бот '+bo+')"><b>'+(tot||'')+'</b>'+segs+'<span>'+dd(x)+'</span></div>';
|
||||||
|
}).join('') : emptyBars;
|
||||||
|
document.getElementById('docs-cap').textContent = '· сайт ' + totSite + ' / бот ' + totBot + ' за 14 дн.';
|
||||||
|
|
||||||
|
// 3) Страницы
|
||||||
|
var mP = barMax(function(x){return x.pages;});
|
||||||
|
var totPages=0;
|
||||||
|
document.getElementById('bars-pages').innerHTML = dl.length ? dl.map(function(x){
|
||||||
|
var p=x.pages||0; totPages+=p;
|
||||||
|
var h=Math.round(p/mP*100);
|
||||||
|
return '<div class="b" style="height:'+h+'%;background:#1baf7a;opacity:1" title="'+x.day+': '+p+' страниц"><b>'+kfmt(p)+'</b><span>'+dd(x)+'</span></div>';
|
||||||
|
}).join('') : emptyBars;
|
||||||
|
document.getElementById('pages-cap').textContent = '· ' + totPages.toLocaleString('ru-RU') + ' за 14 дн.';
|
||||||
|
|
||||||
// Источники трафика
|
// Источники трафика
|
||||||
var sources = d.sources || [];
|
var sources = d.sources || [];
|
||||||
var stot = sources.reduce(function(a,x){ return a + (x.c||0); }, 0) || 1;
|
var stot = sources.reduce(function(a,x){ return a + (x.c||0); }, 0) || 1;
|
||||||
@@ -3471,12 +3522,17 @@ def _admin_collect_stats() -> dict:
|
|||||||
_cutoff = (_today - datetime.timedelta(days=13)).strftime("%Y.%m.%d")
|
_cutoff = (_today - datetime.timedelta(days=13)).strftime("%Y.%m.%d")
|
||||||
files_by_day = {}
|
files_by_day = {}
|
||||||
for r in con.execute(
|
for r in con.execute(
|
||||||
f"SELECT date AS d, {pages_expr} AS pg, source_size_mb AS s, result_size_mb AS rs, elapsed_time AS et "
|
f"SELECT date AS d, {pages_expr} AS pg, source_size_mb AS s, result_size_mb AS rs, "
|
||||||
"FROM FILES WHERE date >= ?", (_cutoff,)):
|
"elapsed_time AS et, user_id AS uid FROM FILES WHERE date >= ?", (_cutoff,)):
|
||||||
key = (r["d"] or "").replace(".", "-")
|
key = (r["d"] or "").replace(".", "-")
|
||||||
e = files_by_day.setdefault(key, {"files": 0, "pages": 0, "src_mb": 0.0,
|
e = files_by_day.setdefault(key, {"files": 0, "files_site": 0, "files_bot": 0,
|
||||||
|
"pages": 0, "src_mb": 0.0,
|
||||||
"res_mb": 0.0, "secs": 0.0, "pages_timed": 0})
|
"res_mb": 0.0, "secs": 0.0, "pages_timed": 0})
|
||||||
e["files"] += 1
|
e["files"] += 1
|
||||||
|
if r["uid"] == WEB_USER_ID:
|
||||||
|
e["files_site"] += 1
|
||||||
|
else:
|
||||||
|
e["files_bot"] += 1
|
||||||
e["pages"] += (r["pg"] or 0)
|
e["pages"] += (r["pg"] or 0)
|
||||||
e["src_mb"] += (r["s"] or 0)
|
e["src_mb"] += (r["s"] or 0)
|
||||||
e["res_mb"] += (r["rs"] or 0)
|
e["res_mb"] += (r["rs"] or 0)
|
||||||
@@ -3491,7 +3547,9 @@ def _admin_collect_stats() -> dict:
|
|||||||
fb = files_by_day.get(dkey, {})
|
fb = files_by_day.get(dkey, {})
|
||||||
ptimed = fb.get("pages_timed", 0)
|
ptimed = fb.get("pages_timed", 0)
|
||||||
daily.append({"day": dkey, "visits": vc, "uniques": udays.get(dkey, 0),
|
daily.append({"day": dkey, "visits": vc, "uniques": udays.get(dkey, 0),
|
||||||
"files": fb.get("files", 0), "pages": fb.get("pages", 0),
|
"files": fb.get("files", 0),
|
||||||
|
"files_site": fb.get("files_site", 0), "files_bot": fb.get("files_bot", 0),
|
||||||
|
"pages": fb.get("pages", 0),
|
||||||
"src_mb": fb.get("src_mb", 0), "res_mb": fb.get("res_mb", 0),
|
"src_mb": fb.get("src_mb", 0), "res_mb": fb.get("res_mb", 0),
|
||||||
"avg_page": (fb.get("secs", 0) / ptimed) if ptimed else 0})
|
"avg_page": (fb.get("secs", 0) / ptimed) if ptimed else 0})
|
||||||
out["daily"] = daily
|
out["daily"] = daily
|
||||||
|
|||||||
Reference in New Issue
Block a user