diff --git a/app/main.py b/app/main.py index d98e00a..ab2d930 100644 --- a/app/main.py +++ b/app/main.py @@ -321,6 +321,15 @@ def _db_ensure_auth_table(): PRIMARY KEY (day, vid) ) """) + # Источники трафика: (день, источник) → сколько заходов. + con.execute(""" + CREATE TABLE IF NOT EXISTS WEB_SOURCES ( + day TEXT, + source TEXT, + count INTEGER DEFAULT 0, + PRIMARY KEY (day, source) + ) + """) # Платежи ЮKassa. status: pending/succeeded/canceled. purpose: donate/api_pro/file/sub. con.execute(""" CREATE TABLE IF NOT EXISTS WEB_PAYMENTS ( @@ -2780,6 +2789,12 @@ _ADMIN_HTML = """
+
+

Источники трафика

+
+
ИсточникЗаходовДоля
+
+

По дням

@@ -2861,6 +2876,14 @@ _ADMIN_HTML = """ document.getElementById('vis-total').textContent = '· всего ' + (d.visits.total||0).toLocaleString('ru-RU') + ' (уник. ' + (d.visits.unique_total||0).toLocaleString('ru-RU') + ')'; + // Источники трафика + var sources = d.sources || []; + var stot = sources.reduce(function(a,x){ return a + (x.c||0); }, 0) || 1; + document.getElementById('t-sources').innerHTML = sources.length ? sources.map(function(x){ + return ''; + }).join('') : ''; + // По дням (свежие сверху) var daily = (d.daily||[]).slice().reverse(); document.getElementById('t-daily').innerHTML = daily.length ? daily.map(function(x){ @@ -2925,8 +2948,45 @@ _ADMIN_HTML = """ # ── Админ-панель ────────────────────────────────────────────────────────────── -def _bump_visit(vid: str | None = None): - """+1 к счётчику заходов за сегодня; vid — id браузера для подсчёта уникальных.""" +_SOURCE_MAP = ( + ("google", "Google"), ("yandex", "Yandex"), ("ya.ru", "Yandex"), + ("t.me", "Telegram"), ("telegram", "Telegram"), ("tgraph", "Telegram"), + ("vk.com", "VK"), ("vk.ru", "VK"), ("bing", "Bing"), ("duckduckgo", "DuckDuckGo"), + ("mail.ru", "Mail.ru"), ("rambler", "Rambler"), ("facebook", "Facebook"), + ("instagram", "Instagram"), ("youtube", "YouTube"), ("t.co", "Twitter/X"), ("x.com", "Twitter/X"), + ("dzen", "Дзен"), ("whatsapp", "WhatsApp"), +) + + +def _source_from(ref: str, url: str) -> str: + """Определяет источник захода: приоритет utm_source из URL, иначе домен referrer.""" + # 1) UTM-метка в адресе страницы + try: + params = _urlparse.parse_qs(_urlparse.urlparse(url or "").query) + utm = (params.get("utm_source") or [""])[0].strip() + if utm: + return utm[:40] + except Exception: + pass + # 2) Referrer + if not ref: + return "Прямые заходы" + try: + host = (_urlparse.urlparse(ref).hostname or "").lower() + except Exception: + host = "" + if host.startswith("www."): + host = host[4:] + if not host or "pdf2scan" in host: # переходы внутри сайта → прямые + return "Прямые заходы" + for key, label in _SOURCE_MAP: + if key in host: + return label + return host[:40] + + +def _bump_visit(vid: str | None = None, source: str | None = None): + """+1 к счётчику заходов за сегодня; vid — id браузера (уникальные), source — источник.""" try: day = datetime.datetime.now().strftime("%Y-%m-%d") with sqlite3.connect(DB_PATH, timeout=10) as con: @@ -2937,6 +2997,11 @@ def _bump_visit(vid: str | None = None): ) if vid: con.execute("INSERT OR IGNORE INTO WEB_VISIT_IDS (day, vid) VALUES (?, ?)", (day, vid)) + if source: + con.execute( + "INSERT INTO WEB_SOURCES (day, source, count) VALUES (?, ?, 1) " + "ON CONFLICT(day, source) DO UPDATE SET count = count + 1", + (day, source)) con.commit() except Exception: pass @@ -2945,13 +3010,20 @@ def _bump_visit(vid: str | None = None): @app.post("/api/hit") async def api_hit(request: Request): """Маячок посещения: JS вызывает раз в сессию браузера. Боты/health-check без JS не считаются. - Cookie p2s_vid (2 года) даёт подсчёт уникальных посетителей.""" + Cookie p2s_vid (2 года) даёт подсчёт уникальных посетителей. Тело {ref,url} → источник.""" vid = request.cookies.get("p2s_vid") or "" new_vid = False if len(vid) < 8: vid = secrets.token_urlsafe(12) new_vid = True - await asyncio.to_thread(_bump_visit, vid) + try: + body = await request.json() + except Exception: + body = {} + if not isinstance(body, dict): + body = {} + source = _source_from(str(body.get("ref") or "")[:500], str(body.get("url") or "")[:500]) + await asyncio.to_thread(_bump_visit, vid, source) resp = JSONResponse({"ok": True}) if new_vid: resp.set_cookie("p2s_vid", vid, max_age=60 * 60 * 24 * 730, @@ -3019,6 +3091,9 @@ def _admin_collect_stats() -> dict: uniq_last7 = sum(udays.get(d["day"], 0) for d in days[:7]) out["visits"] = {"today": today_c, "last7": last7, "total": total, "days": days, "unique_today": uniq_today, "unique_last7": uniq_last7, "unique_total": uniq_total} + # Источники трафика + out["sources"] = [dict(r) for r in con.execute( + "SELECT source, SUM(count) c FROM WEB_SOURCES GROUP BY source ORDER BY c DESC LIMIT 30")] # Документы cols = {r[1] for r in con.execute("PRAGMA table_info(FILES)")} pages_expr = "COALESCE(NULLIF(pages_total,0), num_pages, 0)" if "pages_total" in cols else "COALESCE(num_pages,0)" diff --git a/app/static/index.html b/app/static/index.html index 63f9de4..583b143 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -2839,7 +2839,8 @@ try { if (!sessionStorage.getItem('p2s_hit')) { sessionStorage.setItem('p2s_hit', '1'); - fetch('/api/hit', { method: 'POST', keepalive: true }).catch(function () {}); + fetch('/api/hit', { method: 'POST', keepalive: true, headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ref: document.referrer || '', url: location.href || '' }) }).catch(function () {}); } } catch (_) {} // Возврат после OAuth: чистим ?login и показываем ошибку при неудаче
ДатаЗаходыУник.ФайловСтраницСр/стрИсходникРезультат
'+esc(x.source)+''+(x.c||0).toLocaleString('ru-RU')+ + ''+Math.round((x.c||0)/stot*100)+'%
Пока нет данных — собирается с этого момента