diff --git a/app/main.py b/app/main.py
index d9e378d..68913f9 100644
--- a/app/main.py
+++ b/app/main.py
@@ -3158,6 +3158,10 @@ _ADMIN_HTML = """
Обработано страниц
+
+
Заходы по источникам
+
+
@@ -3317,6 +3321,38 @@ _ADMIN_HTML = """
}).join('') : emptyBars;
document.getElementById('pages-cap').textContent = '· ' + totPages.toLocaleString('ru-RU') + ' за 14 дн.';
+ // 4) Заходы по источникам — стек по источникам (цвет закреплён за источником)
+ var SRC_COLORS = { 'Yandex':'#e34948', 'Google':'#2a78d6', 'Прямые заходы':'#898781',
+ 'Telegram':'#1baf7a', 'VK':'#4a3aa7', 'Mail.ru':'#eb6834' };
+ var SRC_FALLBACK = ['#eda100','#008300','#e87ba4','#eb6834'];
+ var _fbCache = {}, _fbN = 0;
+ function srcColor(n){ if(SRC_COLORS[n]) return SRC_COLORS[n];
+ if(!_fbCache[n]){ _fbCache[n] = SRC_FALLBACK[_fbN % SRC_FALLBACK.length]; _fbN++; } return _fbCache[n]; }
+ var srcNames = d.source_names || [];
+ var mS = 1;
+ dl.forEach(function(x){ var t=0, s=x.sources||{}; srcNames.forEach(function(n){ t+=s[n]||0; }); if(t>mS) mS=t; });
+ var CHS = 78, srcTot = {};
+ document.getElementById('bars-sources').innerHTML = (dl.length && srcNames.length) ? dl.map(function(x){
+ var s = x.sources||{}, tot=0; srcNames.forEach(function(n){ tot += s[n]||0; });
+ var segs='', acc=0;
+ srcNames.forEach(function(n){
+ var v = s[n]||0; if(v<=0) return;
+ srcTot[n] = (srcTot[n]||0) + v;
+ var px = Math.max(2, Math.round(v/mS*CHS));
+ var top = (acc+px >= Math.round(tot/mS*CHS) - 1);
+ segs += '';
+ acc += px;
+ });
+ var lbl = tot ? ''+tot+'' : '';
+ return ''+lbl+segs+''+dd(x)+'
';
+ }).join('') : emptyBars;
+ document.getElementById('src-legend').innerHTML = srcNames.map(function(n){
+ return ''+esc(n)+'';
+ }).join('');
+ var srcSum = srcNames.reduce(function(a,n){ return a + (srcTot[n]||0); }, 0);
+ document.getElementById('src-cap').textContent = '· ' + srcSum + ' за 14 дн. (учёт с 07.07)';
+
// Источники трафика
var sources = d.sources || [];
var stot = sources.reduce(function(a,x){ return a + (x.c||0); }, 0) || 1;
@@ -3752,6 +3788,15 @@ def _admin_collect_stats() -> dict:
if sec is not None:
e["secs"] += sec
e["pages_timed"] += (r["pg"] or 0)
+ # Источники по дням (для графика «заходы по источникам»)
+ _src_cut = (_today - datetime.timedelta(days=13)).strftime("%Y-%m-%d")
+ src_by_day = {}
+ src_totals = {}
+ for r in con.execute(
+ "SELECT day, source, count FROM WEB_SOURCES WHERE day >= ?", (_src_cut,)):
+ src_by_day.setdefault(r["day"], {})[r["source"]] = r["count"]
+ src_totals[r["source"]] = src_totals.get(r["source"], 0) + (r["count"] or 0)
+ out["source_names"] = [s for s, _v in sorted(src_totals.items(), key=lambda kv: -kv[1])]
daily = []
for i in range(13, -1, -1):
dkey = (_today - datetime.timedelta(days=i)).strftime("%Y-%m-%d")
@@ -3763,7 +3808,8 @@ def _admin_collect_stats() -> dict:
"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),
- "avg_page": (fb.get("secs", 0) / ptimed) if ptimed else 0})
+ "avg_page": (fb.get("secs", 0) / ptimed) if ptimed else 0,
+ "sources": src_by_day.get(dkey, {})})
out["daily"] = daily
return out