| Server IP : 138.197.107.151 / Your IP : 216.73.217.7 Web Server : Apache/2.4.58 (Ubuntu) System : Linux BloxBy-Builder 6.8.0-71-generic #71-Ubuntu SMP PREEMPT_DYNAMIC Tue Jul 22 16:52:38 UTC 2025 x86_64 User : wpbetasites_mrakzqskir ( 1022) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /proc/698958/task/698958/cwd/templates/ |
Upload File : |
{% extends "base.html" %}
{% from "_filters.html" import filter_bar with context %}
{% block title %}Dashboard{% endblock %}
{% block body %}
<div class="card">
{{ filter_bar() }}
<p class="muted" style="margin:12px 0 0; font-size:12px;">
{% if days %}
Showing {{ days|length }} day{{ '' if days|length == 1 else 's' }}
({{ days[0] }}{% if days|length > 1 %} to {{ days[-1] }}{% endif %}).
{% if not include_search %}AI activity only — search engines hidden.{% endif %}
{% else %}
No data in that range. Data runs {{ dates[-1] }} to {{ dates[0] }}.
{% endif %}
</p>
{# The whole app anchors on the newest day we hold, not the wall clock, so say
so plainly -- otherwise a range ending days before today looks like a bug. #}
{% if dates %}
<p class="muted" style="margin:4px 0 0; font-size:12px;">
Data current through {{ dates[0] }} — the nightly parse runs a day or
so behind, so this is the latest complete day, not today.
</p>
{% endif %}
</div>
{% if days %}
{% if stranded %}
{# Should be unreachable: categories.py refuses to start with a bot that does
not resolve to a display group. If one ever does slip through, say so
rather than let its hits go missing from the boxes. #}
<div class="note" style="margin-bottom:16px;">
<strong>These bots did not fall into any category box, so their hits are not
counted above:</strong>
{% for plat, row in stranded.items() %}
{{ plat }} ({{ row.hits|commafy }} hits, group "{{ row.group }}"){{ "; " if not loop.last }}
{% endfor %}
— this is a bug, please report it.
</div>
{% endif %}
<div class="card">
<div class="totals">
<div class="big sites">
<div class="n">{{ sites_with_hits|commafy }}</div>
<div class="l"><span class="of">/{{ sites_tracked|commafy }}</span> Sites
<i class="info" data-tip="Sites these bots visited in this date range, out of every site we are capturing logs for. A site with a log file but no AI traffic counts in the total and shows as 0 below.">i</i>
</div>
</div>
<div class="big hits">
<div class="n">{{ totals.hits|commafy }}</div>
<div class="l">Hits
<i class="info" data-tip="Every page request from these bots in this date range. Each hit counts once, in exactly one category.">i</i>
</div>
</div>
{# Four small boxes sit 2x2; adding Search engines makes it five, as 3+2. #}
<div class="grid {{ 'five' if include_search }}">
<div class="mini">
<div class="n">{{ platform_rows|length|commafy }}</div>
<div class="l">Bots
<i class="info" data-tip="How many different crawlers showed up in this date range.">i</i>
</div>
</div>
{% for g in all_groups %}
{% if g != 'search' or include_search %}
<div class="mini {{ g }}">
<div class="n">{{ group_totals.get(g, 0)|commafy }}</div>
<div class="l">{{ group_labels[g] }}
<i class="info" data-tip="{{ group_info[g] }}">i</i>
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
<div class="card">
<h2>By AI platform</h2>
<table>
<thead><tr>
<th>Bot</th><th>Group</th><th class="num">Hits</th><th class="num">Share</th>
<th style="width:190px"></th><th></th>
</tr></thead>
<tbody>
{% set top = platform_rows[0][1].hits if platform_rows else 1 %}
{% for plat, row in platform_rows %}
<tr>
<td>
<span class="rowline">
<strong>{{ plat }}</strong>
<i class="info" data-tip="{{ plat }} — {{ bot_info(plat) }}">i</i>
</span>
</td>
<td><span class="tag {{ row.group }}">{{ group_labels[row.group] }}</span></td>
<td class="num">{{ row.hits|commafy }}</td>
<td class="num muted">{{ '%.1f'|format(row.hits / totals.hits * 100 if totals.hits else 0) }}%</td>
<td><div class="bar {{ 'search' if row.group == 'search' }}"
style="width:{{ (row.hits / top * 100)|round(1) }}%"></div></td>
<td><a href="{{ url_for('detail', **keep(platform=plat)) }}">View hits <span class="arrow">↗</span></a></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="card">
{# Every site is rendered once; filtering and paging happen in the browser.
A few hundred rows is nothing to hold in the DOM, and doing it client-side
is what makes the search instant as you type -- no server round-trip. #}
<div class="tablebar">
<h2>By site ({{ sites_with_hits }} of {{ sites_tracked }})</h2>
<input type="search" id="siteSearch" placeholder="Filter sites…"
autocomplete="off" aria-label="Filter sites">
<span class="count" id="siteCount"></span>
</div>
<table>
<thead><tr>
<th>Site</th><th class="num">Hits</th><th class="num">Share</th>
<th style="width:190px"></th><th></th>
</tr></thead>
<tbody id="siteBody">
{# Rows are sorted by hits, so the first is the widest bar. `or 1` keeps the
division alive when nothing in range has any hits. #}
{% set stop = (site_rows[0].hits if site_rows else 1) or 1 %}
{% for row in site_rows %}
{# Both names are searchable: people look for a site by its domain, but a
zero-hit site can only be named by its install. #}
<tr data-site="{{ row.name|lower }} {{ row.install|lower }}"
class="{{ 'zero' if not row.hits }}">
<td>
{% if row.hits %}
<a href="{{ url_for('detail', **keep(site=row.install)) }}">{{ row.name }}</a>
{% else %}
<span class="muted">{{ row.name }}</span>
{% endif %}
{% if row.extra %}
<span class="more" title="This install also serves {{ row.extra }} other domain{{ '' if row.extra == 1 else 's' }}; their hits are counted here.">+{{ row.extra }}</span>
{% endif %}
</td>
<td class="num">{{ row.hits|commafy }}</td>
<td class="num muted">{{ '%.1f'|format(row.hits / totals.hits * 100 if totals.hits else 0) }}%</td>
<td><div class="bar" style="width:{{ (row.hits / stop * 100)|round(1) }}%"></div></td>
<td>
{% if row.hits %}
<a href="{{ url_for('detail', **keep(site=row.install)) }}">View hits <span class="arrow">↗</span></a>
{% else %}
<span class="muted" title="We are capturing this site's logs; no AI crawler hit it in this range.">No AI traffic</span>
{% endif %}
</td>
</tr>
{% endfor %}
<tr id="siteEmpty" hidden><td colspan="5" class="empty">No sites match that filter.</td></tr>
</tbody>
</table>
<div class="pager" id="sitePager"></div>
</div>
<script>
(function () {
var PER_PAGE = 25;
var body = document.getElementById('siteBody');
if (!body) return;
var rows = Array.prototype.slice.call(body.querySelectorAll('tr[data-site]'));
var empty = document.getElementById('siteEmpty');
var search = document.getElementById('siteSearch');
var pager = document.getElementById('sitePager');
var count = document.getElementById('siteCount');
var page = 1;
var matches = rows; // rows arrive sorted by hits, highest first
function button(label, target, opts) {
var b = document.createElement('button');
b.type = 'button';
b.textContent = label;
if (opts && opts.disabled) b.disabled = true;
if (opts && opts.current) b.className = 'on';
b.addEventListener('click', function () { page = target; render(); });
return b;
}
function pageNumbers(pages) {
// First, last, and a window around the current page; gaps become an ellipsis
// so 300 sites do not print 13+ buttons in a row.
var want = {1: 1, 2: 1};
want[pages] = 1; want[pages - 1] = 1;
for (var d = -1; d <= 1; d++) want[page + d] = 1;
var out = [], prev = 0;
Object.keys(want).map(Number).filter(function (n) { return n >= 1 && n <= pages; })
.sort(function (a, b) { return a - b; })
.forEach(function (n) {
if (prev && n - prev > 1) out.push(null); // ellipsis
out.push(n); prev = n;
});
return out;
}
function render() {
var pages = Math.max(1, Math.ceil(matches.length / PER_PAGE));
if (page > pages) page = pages;
if (page < 1) page = 1;
rows.forEach(function (r) { r.hidden = true; });
var start = (page - 1) * PER_PAGE;
matches.slice(start, start + PER_PAGE).forEach(function (r) { r.hidden = false; });
empty.hidden = matches.length !== 0;
count.textContent = matches.length === rows.length
? rows.length + ' sites'
: matches.length + ' of ' + rows.length + ' sites';
pager.innerHTML = '';
if (matches.length === 0) return;
pager.appendChild(button('‹ Prev', page - 1, {disabled: page === 1}));
pageNumbers(pages).forEach(function (n) {
if (n === null) {
var s = document.createElement('span');
s.className = 'gap'; s.textContent = '…';
pager.appendChild(s);
} else {
pager.appendChild(button(String(n), n, {current: n === page}));
}
});
pager.appendChild(button('Next ›', page + 1, {disabled: page === pages}));
}
search.addEventListener('input', function () {
var q = search.value.trim().toLowerCase();
matches = q ? rows.filter(function (r) {
return r.getAttribute('data-site').indexOf(q) !== -1; // matches anywhere
}) : rows;
page = 1; // a narrower list makes the old page number meaningless
render();
});
render();
})();
</script>
{% endif %}
{% endblock %}