Админка: время обработки по каждому файлу и среднее с/страницу по дням
- «По дням»: колонка «Ср/стр» (сумма elapsed по дню / страницы дня) — динамика скорости - «Обработанные документы»: колонка «Время» (elapsed_time файла) + в CSV - Общий парсер _elapsed_to_sec (H:MM:SS.ffffff → сек), переиспользован в итогах/дне/файле Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
68
app/main.py
68
app/main.py
@@ -2782,7 +2782,7 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
|
|
||||||
<section>
|
<section>
|
||||||
<h2>По дням</h2>
|
<h2>По дням</h2>
|
||||||
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead>
|
<div class="scroll"><table><thead><tr><th>Дата</th><th class="num">Заходы</th><th class="num">Уник.</th><th class="num">Файлов</th><th class="num">Страниц</th><th class="num">Ср/стр</th><th class="num">Исходник</th><th class="num">Результат</th></tr></thead>
|
||||||
<tbody id="t-daily"></tbody></table></div>
|
<tbody id="t-daily"></tbody></table></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -2797,7 +2797,7 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
<h2 style="margin:0">Обработанные документы <span class="muted" id="docs-totals"></span></h2>
|
<h2 style="margin:0">Обработанные документы <span class="muted" id="docs-totals"></span></h2>
|
||||||
<button class="ghost" onclick="downloadCsv()">Экспорт CSV</button>
|
<button class="ghost" onclick="downloadCsv()">Экспорт CSV</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="scroll"><table><thead><tr><th>Дата</th><th>Файл</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>
|
||||||
</div>
|
</div>
|
||||||
@@ -2809,6 +2809,7 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
function money(v){ return Math.round(v||0).toLocaleString('ru-RU') + ' ₽'; }
|
function money(v){ return Math.round(v||0).toLocaleString('ru-RU') + ' ₽'; }
|
||||||
function size(mb){ if(mb==null) return '—'; mb=+mb; if(!mb) return '—'; return mb>=1024 ? (mb/1024).toFixed(2)+' ГБ' : mb.toFixed(1)+' МБ'; }
|
function size(mb){ if(mb==null) return '—'; mb=+mb; if(!mb) return '—'; return mb>=1024 ? (mb/1024).toFixed(2)+' ГБ' : mb.toFixed(1)+' МБ'; }
|
||||||
function dur(sec){ sec=Math.round(sec||0); if(sec<60) return sec+' с'; var m=Math.floor(sec/60); if(m<60) return m+' мин'; var h=Math.floor(m/60); if(h<24) return h+' ч '+(m%60)+' мин'; return Math.floor(h/24)+' дн '+(h%24)+' ч'; }
|
function dur(sec){ sec=Math.round(sec||0); if(sec<60) return sec+' с'; var m=Math.floor(sec/60); if(m<60) return m+' мин'; var h=Math.floor(m/60); if(h<24) return h+' ч '+(m%60)+' мин'; return Math.floor(h/24)+' дн '+(h%24)+' ч'; }
|
||||||
|
function psec(s){ if(s==null) return '—'; s=+s; if(s<60) return s.toFixed(1)+' с'; var m=Math.floor(s/60); return m+' м '+Math.round(s%60)+' с'; }
|
||||||
function dts(ts){ if(!ts) return '—'; var d=new Date(ts*1000); return d.toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); }
|
function dts(ts){ if(!ts) return '—'; var d=new Date(ts*1000); return d.toLocaleString('ru-RU',{day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'}); }
|
||||||
function dOnly(ts){ if(!ts) return '—'; var d=new Date(ts*1000); return d.toLocaleDateString('ru-RU'); }
|
function dOnly(ts){ if(!ts) return '—'; var d=new Date(ts*1000); return d.toLocaleDateString('ru-RU'); }
|
||||||
function daysLeft(ts){ var s=Math.max(0,(ts*1000-Date.now())/86400000); return s>=365 ? Math.round(s/365)+' г.' : Math.round(s)+' дн.'; }
|
function daysLeft(ts){ var s=Math.max(0,(ts*1000-Date.now())/86400000); return s>=365 ? Math.round(s/365)+' г.' : Math.round(s)+' дн.'; }
|
||||||
@@ -2866,8 +2867,9 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
var dd = x.day.slice(8)+'.'+x.day.slice(5,7)+'.'+x.day.slice(0,4);
|
var dd = x.day.slice(8)+'.'+x.day.slice(5,7)+'.'+x.day.slice(0,4);
|
||||||
return '<tr><td>'+dd+'</td><td class="num">'+(x.visits||0)+'</td><td class="num">'+(x.uniques||0)+
|
return '<tr><td>'+dd+'</td><td class="num">'+(x.visits||0)+'</td><td class="num">'+(x.uniques||0)+
|
||||||
'</td><td class="num">'+(x.files||0)+'</td><td class="num">'+(x.pages||0).toLocaleString('ru-RU')+
|
'</td><td class="num">'+(x.files||0)+'</td><td class="num">'+(x.pages||0).toLocaleString('ru-RU')+
|
||||||
|
'</td><td class="num">'+(x.avg_page?psec(x.avg_page):'—')+
|
||||||
'</td><td class="num">'+size(x.src_mb)+'</td><td class="num">'+size(x.res_mb)+'</td></tr>';
|
'</td><td class="num">'+size(x.src_mb)+'</td><td class="num">'+size(x.res_mb)+'</td></tr>';
|
||||||
}).join('') : '<tr><td colspan="7" class="muted">Пока нет данных</td></tr>';
|
}).join('') : '<tr><td colspan="8" class="muted">Пока нет данных</td></tr>';
|
||||||
|
|
||||||
// Платежи
|
// Платежи
|
||||||
var bp = d.payments.by_purpose || {};
|
var bp = d.payments.by_purpose || {};
|
||||||
@@ -2890,9 +2892,9 @@ _ADMIN_HTML = """<!doctype html>
|
|||||||
var srcPill = web ? '<span class="pill web">сайт</span>' : '<span class="pill bot">бот</span>';
|
var srcPill = web ? '<span class="pill web">сайт</span>' : '<span class="pill bot">бот</span>';
|
||||||
var dd = (f.date||'') + (f.time ? ' '+String(f.time).slice(0,5) : '');
|
var dd = (f.date||'') + (f.time ? ' '+String(f.time).slice(0,5) : '');
|
||||||
return '<tr><td>'+esc(dd)+'</td><td class="fname" title="'+esc(f.name)+'">'+esc(f.name)+'</td><td class="num">'+(f.pages||'—')+
|
return '<tr><td>'+esc(dd)+'</td><td class="fname" title="'+esc(f.name)+'">'+esc(f.name)+'</td><td class="num">'+(f.pages||'—')+
|
||||||
'</td><td class="num">'+size(f.source_size_mb)+'</td><td class="num">'+size(f.result_size_mb)+
|
'</td><td class="num">'+psec(f.secs)+'</td><td class="num">'+size(f.source_size_mb)+'</td><td class="num">'+size(f.result_size_mb)+
|
||||||
'</td><td>'+esc(f.quality||'—')+'</td><td>'+esc(f.stitching||'—')+'</td><td>'+(f.ocr?'✓':'')+'</td><td>'+srcPill+'</td></tr>';
|
'</td><td>'+esc(f.quality||'—')+'</td><td>'+esc(f.stitching||'—')+'</td><td>'+(f.ocr?'✓':'')+'</td><td>'+srcPill+'</td></tr>';
|
||||||
}).join('') : '<tr><td colspan="9" class="muted">Пока нет документов</td></tr>';
|
}).join('') : '<tr><td colspan="10" class="muted">Пока нет документов</td></tr>';
|
||||||
}
|
}
|
||||||
|
|
||||||
function showGate(err){
|
function showGate(err){
|
||||||
@@ -2967,6 +2969,19 @@ def _require_admin(request: Request) -> bool:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _elapsed_to_sec(et):
|
||||||
|
"""Парсит elapsed_time ('H:MM:SS.ffffff') в секунды; None если не распарсилось."""
|
||||||
|
if not et:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
pp = str(et).split(":")
|
||||||
|
if len(pp) == 3:
|
||||||
|
return int(pp[0]) * 3600 + int(pp[1]) * 60 + float(pp[2])
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _admin_collect_stats() -> dict:
|
def _admin_collect_stats() -> dict:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
out = {"generated_at": now, "web_user_id": WEB_USER_ID}
|
out = {"generated_at": now, "web_user_id": WEB_USER_ID}
|
||||||
@@ -3009,8 +3024,10 @@ def _admin_collect_stats() -> dict:
|
|||||||
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
|
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
|
||||||
docs = [dict(r) for r in con.execute(
|
docs = [dict(r) for r in con.execute(
|
||||||
f"SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, {pages_expr} AS pages, "
|
f"SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, {pages_expr} AS pages, "
|
||||||
"source_size_mb, result_size_mb, quality, stitching, ocr, user_id, url_res_f "
|
"source_size_mb, result_size_mb, quality, stitching, ocr, elapsed_time, user_id, url_res_f "
|
||||||
"FROM FILES ORDER BY rowid DESC LIMIT 150")]
|
"FROM FILES ORDER BY rowid DESC LIMIT 150")]
|
||||||
|
for _d in docs:
|
||||||
|
_d["secs"] = _elapsed_to_sec(_d.get("elapsed_time"))
|
||||||
agg = con.execute(
|
agg = con.execute(
|
||||||
f"SELECT COUNT(*) c, COALESCE(SUM({pages_expr}),0) p, "
|
f"SELECT COUNT(*) c, COALESCE(SUM({pages_expr}),0) p, "
|
||||||
"COALESCE(SUM(source_size_mb),0) ssz, COALESCE(SUM(result_size_mb),0) rsz FROM FILES").fetchone()
|
"COALESCE(SUM(source_size_mb),0) ssz, COALESCE(SUM(result_size_mb),0) rsz FROM FILES").fetchone()
|
||||||
@@ -3020,13 +3037,10 @@ def _admin_collect_stats() -> dict:
|
|||||||
for r in con.execute(
|
for r in con.execute(
|
||||||
f"SELECT elapsed_time AS et, {pages_expr} AS pg FROM FILES "
|
f"SELECT elapsed_time AS et, {pages_expr} AS pg FROM FILES "
|
||||||
"WHERE elapsed_time IS NOT NULL AND TRIM(elapsed_time) NOT IN ('','-')"):
|
"WHERE elapsed_time IS NOT NULL AND TRIM(elapsed_time) NOT IN ('','-')"):
|
||||||
try:
|
s = _elapsed_to_sec(r["et"])
|
||||||
pp = str(r["et"]).split(":")
|
if s is not None:
|
||||||
if len(pp) == 3:
|
tot_sec += s
|
||||||
tot_sec += int(pp[0]) * 3600 + int(pp[1]) * 60 + float(pp[2])
|
pages_timed += (r["pg"] or 0)
|
||||||
pages_timed += (r["pg"] or 0)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
out["docs"] = {"recent": docs, "total_count": agg["c"], "total_pages": agg["p"],
|
out["docs"] = {"recent": docs, "total_count": agg["c"], "total_pages": agg["p"],
|
||||||
"total_source_mb": agg["ssz"], "total_result_mb": agg["rsz"],
|
"total_source_mb": agg["ssz"], "total_result_mb": agg["rsz"],
|
||||||
"total_seconds": tot_sec, "avg_page_seconds": (tot_sec / pages_timed) if pages_timed else 0}
|
"total_seconds": tot_sec, "avg_page_seconds": (tot_sec / pages_timed) if pages_timed else 0}
|
||||||
@@ -3035,19 +3049,29 @@ 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, COUNT(*) c, COALESCE(SUM({pages_expr}),0) p, "
|
f"SELECT date AS d, {pages_expr} AS pg, source_size_mb AS s, result_size_mb AS rs, elapsed_time AS et "
|
||||||
"COALESCE(SUM(source_size_mb),0) s, COALESCE(SUM(result_size_mb),0) rs "
|
"FROM FILES WHERE date >= ?", (_cutoff,)):
|
||||||
"FROM FILES WHERE date >= ? GROUP BY date", (_cutoff,)):
|
key = (r["d"] or "").replace(".", "-")
|
||||||
files_by_day[(r["d"] or "").replace(".", "-")] = {
|
e = files_by_day.setdefault(key, {"files": 0, "pages": 0, "src_mb": 0.0,
|
||||||
"files": r["c"], "pages": r["p"], "src_mb": r["s"], "res_mb": r["rs"]}
|
"res_mb": 0.0, "secs": 0.0, "pages_timed": 0})
|
||||||
|
e["files"] += 1
|
||||||
|
e["pages"] += (r["pg"] or 0)
|
||||||
|
e["src_mb"] += (r["s"] or 0)
|
||||||
|
e["res_mb"] += (r["rs"] or 0)
|
||||||
|
sec = _elapsed_to_sec(r["et"])
|
||||||
|
if sec is not None:
|
||||||
|
e["secs"] += sec
|
||||||
|
e["pages_timed"] += (r["pg"] or 0)
|
||||||
daily = []
|
daily = []
|
||||||
for i in range(13, -1, -1):
|
for i in range(13, -1, -1):
|
||||||
dkey = (_today - datetime.timedelta(days=i)).strftime("%Y-%m-%d")
|
dkey = (_today - datetime.timedelta(days=i)).strftime("%Y-%m-%d")
|
||||||
vc = next((x["count"] for x in days if x["day"] == dkey), 0)
|
vc = next((x["count"] for x in days if x["day"] == dkey), 0)
|
||||||
fb = files_by_day.get(dkey, {})
|
fb = files_by_day.get(dkey, {})
|
||||||
|
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), "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})
|
||||||
out["daily"] = daily
|
out["daily"] = daily
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -3063,7 +3087,7 @@ def _build_docs_csv() -> bytes:
|
|||||||
import csv, io
|
import csv, io
|
||||||
buf = io.StringIO()
|
buf = io.StringIO()
|
||||||
w = csv.writer(buf, delimiter=";")
|
w = csv.writer(buf, delimiter=";")
|
||||||
w.writerow(["Дата", "Время", "Файл", "Страниц", "Исходник, МБ",
|
w.writerow(["Дата", "Время", "Файл", "Страниц", "Время обраб., с", "Исходник, МБ",
|
||||||
"Результат, МБ", "Качество", "Сшивка", "OCR", "Источник", "Статус"])
|
"Результат, МБ", "Качество", "Сшивка", "OCR", "Источник", "Статус"])
|
||||||
def _mb(v):
|
def _mb(v):
|
||||||
return ("%.2f" % v) if v else ""
|
return ("%.2f" % v) if v else ""
|
||||||
@@ -3073,9 +3097,11 @@ def _build_docs_csv() -> bytes:
|
|||||||
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
|
pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)"
|
||||||
for r in con.execute(
|
for r in con.execute(
|
||||||
f"SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, {pages_expr} AS pages, "
|
f"SELECT date, time, COALESCE(NULLIF(original_name,''), name_or_f) AS name, {pages_expr} AS pages, "
|
||||||
"source_size_mb, result_size_mb, quality, stitching, ocr, user_id, url_res_f FROM FILES ORDER BY rowid DESC"):
|
"source_size_mb, result_size_mb, quality, stitching, ocr, elapsed_time, user_id, url_res_f FROM FILES ORDER BY rowid DESC"):
|
||||||
src = "сайт" if (r["user_id"] == WEB_USER_ID) else "бот"
|
src = "сайт" if (r["user_id"] == WEB_USER_ID) else "бот"
|
||||||
|
_et = _elapsed_to_sec(r["elapsed_time"])
|
||||||
w.writerow([r["date"] or "", (r["time"] or "")[:8], r["name"] or "", r["pages"] or "",
|
w.writerow([r["date"] or "", (r["time"] or "")[:8], r["name"] or "", r["pages"] or "",
|
||||||
|
("%.1f" % _et) if _et is not None else "",
|
||||||
_mb(r["source_size_mb"]), _mb(r["result_size_mb"]), r["quality"] or "",
|
_mb(r["source_size_mb"]), _mb(r["result_size_mb"]), r["quality"] or "",
|
||||||
r["stitching"] or "", "да" if r["ocr"] else "", src, r["url_res_f"] or ""])
|
r["stitching"] or "", "да" if r["ocr"] else "", src, r["url_res_f"] or ""])
|
||||||
return ("" + buf.getvalue()).encode("utf-8") # BOM → кириллица в Excel
|
return ("" + buf.getvalue()).encode("utf-8") # BOM → кириллица в Excel
|
||||||
|
|||||||
Reference in New Issue
Block a user