From 4507c4bad329deb9a6f28a15c83b7d2d6189ad9c Mon Sep 17 00:00:00 2001 From: berkio_admin_gitea Date: Tue, 7 Jul 2026 13:23:31 +0000 Subject: [PATCH] =?UTF-8?q?=D0=90=D0=B4=D0=BC=D0=B8=D0=BD=D0=BA=D0=B0:=20?= =?UTF-8?q?=D0=B2=D1=80=D0=B5=D0=BC=D1=8F=20=D0=BE=D0=B1=D1=80=D0=B0=D0=B1?= =?UTF-8?q?=D0=BE=D1=82=D0=BA=D0=B8=20=D0=BF=D0=BE=20=D0=BA=D0=B0=D0=B6?= =?UTF-8?q?=D0=B4=D0=BE=D0=BC=D1=83=20=D1=84=D0=B0=D0=B9=D0=BB=D1=83=20?= =?UTF-8?q?=D0=B8=20=D1=81=D1=80=D0=B5=D0=B4=D0=BD=D0=B5=D0=B5=20=D1=81/?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B0=D0=BD=D0=B8=D1=86=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=20=D0=B4=D0=BD=D1=8F=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - «По дням»: колонка «Ср/стр» (сумма elapsed по дню / страницы дня) — динамика скорости - «Обработанные документы»: колонка «Время» (elapsed_time файла) + в CSV - Общий парсер _elapsed_to_sec (H:MM:SS.ffffff → сек), переиспользован в итогах/дне/файле Co-Authored-By: Claude Opus 4.8 --- app/main.py | 68 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 21 deletions(-) diff --git a/app/main.py b/app/main.py index 5c16b24..d98e00a 100644 --- a/app/main.py +++ b/app/main.py @@ -2782,7 +2782,7 @@ _ADMIN_HTML = """

По дням

-
+
ДатаЗаходыУник.ФайловСтраницИсходникРезультат
ДатаЗаходыУник.ФайловСтраницСр/стрИсходникРезультат
@@ -2797,7 +2797,7 @@ _ADMIN_HTML = """

Обработанные документы

-
+
ДатаФайлСтр.ИсходникРезультатКач-воСшивкаOCRИсточник
ДатаФайлСтр.ВремяИсходникРезультатКач-воСшивкаOCRИсточник
@@ -2809,6 +2809,7 @@ _ADMIN_HTML = """ 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 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 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)+' дн.'; } @@ -2866,8 +2867,9 @@ _ADMIN_HTML = """ var dd = x.day.slice(8)+'.'+x.day.slice(5,7)+'.'+x.day.slice(0,4); return ''+dd+''+(x.visits||0)+''+(x.uniques||0)+ ''+(x.files||0)+''+(x.pages||0).toLocaleString('ru-RU')+ + ''+(x.avg_page?psec(x.avg_page):'—')+ ''+size(x.src_mb)+''+size(x.res_mb)+''; - }).join('') : 'Пока нет данных'; + }).join('') : 'Пока нет данных'; // Платежи var bp = d.payments.by_purpose || {}; @@ -2890,9 +2892,9 @@ _ADMIN_HTML = """ var srcPill = web ? 'сайт' : 'бот'; var dd = (f.date||'') + (f.time ? ' '+String(f.time).slice(0,5) : ''); return ''+esc(dd)+''+esc(f.name)+''+(f.pages||'—')+ - ''+size(f.source_size_mb)+''+size(f.result_size_mb)+ + ''+psec(f.secs)+''+size(f.source_size_mb)+''+size(f.result_size_mb)+ ''+esc(f.quality||'—')+''+esc(f.stitching||'—')+''+(f.ocr?'✓':'')+''+srcPill+''; - }).join('') : 'Пока нет документов'; + }).join('') : 'Пока нет документов'; } function showGate(err){ @@ -2967,6 +2969,19 @@ def _require_admin(request: Request) -> bool: 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: now = time.time() 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)" 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, " - "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")] + for _d in docs: + _d["secs"] = _elapsed_to_sec(_d.get("elapsed_time")) agg = con.execute( 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() @@ -3020,13 +3037,10 @@ def _admin_collect_stats() -> dict: for r in con.execute( f"SELECT elapsed_time AS et, {pages_expr} AS pg FROM FILES " "WHERE elapsed_time IS NOT NULL AND TRIM(elapsed_time) NOT IN ('','-')"): - try: - pp = str(r["et"]).split(":") - if len(pp) == 3: - tot_sec += int(pp[0]) * 3600 + int(pp[1]) * 60 + float(pp[2]) - pages_timed += (r["pg"] or 0) - except Exception: - pass + s = _elapsed_to_sec(r["et"]) + if s is not None: + tot_sec += s + pages_timed += (r["pg"] or 0) out["docs"] = {"recent": docs, "total_count": agg["c"], "total_pages": agg["p"], "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} @@ -3035,19 +3049,29 @@ def _admin_collect_stats() -> dict: _cutoff = (_today - datetime.timedelta(days=13)).strftime("%Y.%m.%d") files_by_day = {} for r in con.execute( - f"SELECT date AS d, COUNT(*) c, COALESCE(SUM({pages_expr}),0) p, " - "COALESCE(SUM(source_size_mb),0) s, COALESCE(SUM(result_size_mb),0) rs " - "FROM FILES WHERE date >= ? GROUP BY date", (_cutoff,)): - files_by_day[(r["d"] or "").replace(".", "-")] = { - "files": r["c"], "pages": r["p"], "src_mb": r["s"], "res_mb": r["rs"]} + f"SELECT date AS d, {pages_expr} AS pg, source_size_mb AS s, result_size_mb AS rs, elapsed_time AS et " + "FROM FILES WHERE date >= ?", (_cutoff,)): + key = (r["d"] or "").replace(".", "-") + e = files_by_day.setdefault(key, {"files": 0, "pages": 0, "src_mb": 0.0, + "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 = [] for i in range(13, -1, -1): dkey = (_today - datetime.timedelta(days=i)).strftime("%Y-%m-%d") vc = next((x["count"] for x in days if x["day"] == dkey), 0) fb = files_by_day.get(dkey, {}) + ptimed = fb.get("pages_timed", 0) daily.append({"day": dkey, "visits": vc, "uniques": udays.get(dkey, 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 return out @@ -3063,7 +3087,7 @@ def _build_docs_csv() -> bytes: import csv, io buf = io.StringIO() w = csv.writer(buf, delimiter=";") - w.writerow(["Дата", "Время", "Файл", "Страниц", "Исходник, МБ", + w.writerow(["Дата", "Время", "Файл", "Страниц", "Время обраб., с", "Исходник, МБ", "Результат, МБ", "Качество", "Сшивка", "OCR", "Источник", "Статус"]) def _mb(v): 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)" for r in con.execute( 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 "бот" + _et = _elapsed_to_sec(r["elapsed_time"]) 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 "", r["stitching"] or "", "да" if r["ocr"] else "", src, r["url_res_f"] or ""]) return ("" + buf.getvalue()).encode("utf-8") # BOM → кириллица в Excel