<?php
/*
 * Owner/admin-only guide: the Visitor Engine (Analytics).
 * Served by modules/Help/HelpController (topic = "visitor_engine").
 * $c() prints an HTML-safe code block from a nowdoc so samples containing
 * <, > and & display literally.
 */
$c = static fn (string $s): string => '<pre><code>' . htmlspecialchars($s) . '</code></pre>';
?>

<h1><i class="bi bi-graph-up-arrow"></i> Visitor Engine (Analytics)</h1>
<p class="lead">One privacy-respecting analytics reporter for your <em>entire</em> system — the console,
the licence API, the update host, the marketing site and the community portal — with accurate
location and device detection and IP databases that keep themselves current.</p>

<div class="alert alert-info">
    <strong>Where is it?</strong> Owner menu &rarr; <strong>Visitors</strong> (<code>/platform/visitors</code>).
    Owner-only. Nothing here is sent to a third party unless you explicitly turn on the API fallback.
</div>

<h2>Contents</h2>
<ul>
    <li><a href="#how">How it works &mdash; three ways data arrives</a></li>
    <li><a href="#accuracy">Accurate location &amp; device (and two honest limits)</a></li>
    <li><a href="#geodb">The offline IP databases &amp; auto-update</a></li>
    <li><a href="#channels">Channels &amp; the Clients &amp; API view</a></li>
    <li><a href="#servers">By server &mdash; primary vs backups</a></li>
    <li><a href="#properties">Adding a property (marketing, community, &hellip;)</a></li>
    <li><a href="#community">Wiring the community portal (PHP)</a></li>
    <li><a href="#serverside">Server-side reporting from PHP</a></li>
    <li><a href="#privacy">Privacy, retention &amp; bots</a></li>
    <li><a href="#cron">Cron &amp; scheduled jobs</a></li>
    <li><a href="#trouble">Troubleshooting</a></li>
</ul>

<h2 id="how">How it works &mdash; three ways data arrives</h2>
<ol>
    <li><strong>The console records itself.</strong> Every page load on this install is logged once per
        session (then counters only) by <code>VisitorLog::record()</code> in <code>public/index.php</code>.</li>
    <li><strong>A tiny browser beacon</strong> (<code>pp-visit.js</code> on the console, <code>pp-analytics.js</code>
        on other properties) adds what only the browser knows &mdash; timezone, screen, connection, engaged time.</li>
    <li><strong>API calls log themselves.</strong> Licence and update-host requests are captured as
        <em>client queries</em> via a shutdown hook, so the final status is recorded even when the handler exits.</li>
</ol>
<p>Separate properties (marketing site, community portal, backups) report into this one dashboard through the
    <strong>collector</strong> at <code>/api/analytics/collect</code> &mdash; the same model Umami and Plausible use.</p>

<h2 id="accuracy">Accurate location &amp; device</h2>
<p>Location comes from a <strong>real IP lookup</strong> in a local database &mdash; never a guess from the
    browser's language (that old guess is why a Nepali visitor once showed as American). Device comes from
    <strong>User-Agent Client Hints</strong> (exact browser + version, OS + version, and the device model like
    &ldquo;Pixel 8&rdquo;), falling back to the User-Agent string for Safari/Firefox.</p>
<div class="alert alert-warning">
    <strong>Two limits we never fake:</strong>
    <ul class="mb-0">
        <li>A <strong>MAC address</strong> cannot be read over the web &mdash; routers strip it at the first hop. No column pretends to show it.</li>
        <li><strong>Laptop vs desktop</strong> cannot be told apart &mdash; the signals are identical. We report
            <em>mobile / tablet / desktop</em> plus the real device model where the browser exposes it.</li>
    </ul>
</div>

<h2 id="geodb">The offline IP databases &amp; auto-update</h2>
<p>Two free, no-registration databases live in <code>storage/geoip/</code>:</p>
<ul>
    <li><strong>Country</strong> (<code>dbip-country-lite.mmdb</code>) &mdash; the visitor's country/region.</li>
    <li><strong>Network / ASN</strong> (<code>dbip-asn-lite.mmdb</code>) &mdash; the organisation/ISP and a
        <strong>hosting/VPN flag</strong> that separates a real person's ISP from a datacenter (a strong bot signal).</li>
</ul>
<p>Both <strong>update themselves</strong>: once a day the engine checks, and downloads a fresher build only when
    the installed one is over 30 days old (DB-IP publishes monthly). The multi-MB download runs <em>after</em> the
    response, so no visitor ever waits. You can also click <strong>Settings &rarr; Download / update</strong> any time,
    or drop your own MaxMind GeoLite2 <code>.mmdb</code> into <code>storage/geoip/</code>.</p>
<p>For deterministic scheduling, add the CLI to cron (see <a href="#cron">Cron</a>).</p>

<h2 id="channels">Channels &amp; the Clients &amp; API view</h2>
<p>Every record is tagged with a <strong>channel</strong>:</p>
<ul>
    <li><code>console</code> &mdash; this admin console.</li>
    <li><code>marketing</code> / <code>community</code> &mdash; properties reporting via the collector.</li>
    <li><code>license</code> / <code>update</code> &mdash; server-to-server API callers.</li>
</ul>
<p>The <strong>Clients &amp; API</strong> panel groups every server-to-server caller into one row per client
    install &mdash; where it calls from, what it does (sync / renew / activate&hellip;), and its success rate. It
    also splits callers into <strong>end clients</strong> vs <strong>backup servers</strong> (a caller whose IP or
    host matches your licence peer mesh is flagged <em>backup</em>). This complements the licensing system's own
    records &mdash; it is the traffic/health view, not the source of truth for entitlements.</p>

<h2 id="servers">By server &mdash; primary vs backups</h2>
<p>Each record remembers the <strong>server that observed it</strong>. Set this install's label under
    <strong>Settings &rarr; This server's label</strong> (default: role + hostname, e.g.
    <code>primary &middot; ppphp.ppradhan.com</code>). When your backups report in (below), the
    <strong>By server</strong> breakdown shows traffic split across primary and each backup.</p>

<h2 id="properties">Adding a property</h2>
<p>On the Visitors page, open <strong>Properties &rarr; Add property</strong>. You get:</p>
<ul>
    <li>a <strong>browser snippet</strong> (a public <em>Site ID</em>) to paste on the site, and</li>
    <li>a <strong>server secret</strong> (shown once) for PHP-side reporting.</li>
</ul>
<p>For a <strong>static site</strong> (e.g. the marketing site's <code>index.html</code>), paste the snippet before
    <code>&lt;/body&gt;</code>:</p>
<?= $c('<script>window.__PP_ANALYTICS={url:"https://ppphp.ppradhan.com/public/api/analytics/collect",site:"st_xxxxxxxx"};</script>' . "\n" . '<script src="https://ppphp.ppradhan.com/public/assets/js/pp-analytics.js" defer></script>') ?>

<h2 id="community">Wiring the community portal (PHP)</h2>
<p>The forum is server-rendered PHP, so you don't paste HTML into a page &mdash; you set the Site ID once and the
    shared layout emits the beacon on every page for you. It's already built in:</p>
<ol>
    <li>In the Visitors console, add a property with channel <strong>community</strong> and copy its <em>Site ID</em>.</li>
    <li>Open the forum's <strong>Admin &rarr; Site settings</strong> and paste the Site ID into
        <strong>Visitor analytics &mdash; Site ID</strong>. Leave the collector URL blank to use the main server.</li>
    <li>Save. Every forum page (including the login/register pages) now reports in. Blank the Site ID to turn it off.</li>
</ol>
<p>Under the hood this is <code>forum_analytics_beacon()</code> in <code>_shell.php</code>, called from both footers &mdash;
    nothing to edit by hand.</p>

<h2 id="serverside">Server-side reporting from PHP</h2>
<p>When you want a property's <em>server</em> to report (for example a backup forwarding its API-client telemetry, or a
    page you render without a browser beacon), configure the client once and call it:</p>
<?= $c("use PpPhp\\Core\\Analytics\\AnalyticsClient;\n\n// once, e.g. in the property's bootstrap:\nAnalyticsClient::configure('https://ppphp.ppradhan.com/public/api/analytics/collect', 'st_xxxxxxxx', 'THE-SECRET');\n\n// a server-rendered pageview (pass the real visitor IP):\nAnalyticsClient::reportPageview(\$visitorIp, 'https://community.example/questions/42', \$referrer);\n\n// forward an API-client call to the primary's Clients & API view:\nAnalyticsClient::reportApi('sync', 200, 'client.example', 'instance-id', \$clientIp);") ?>
<p>Reports are fire-and-forget and sent after the response, so a slow collector never delays the page.</p>

<h2 id="privacy">Privacy, retention &amp; bots</h2>
<ul>
    <li><strong>Do-Not-Track / Global-Privacy-Control</strong> are honoured (the IP is anonymised for those visitors).</li>
    <li><strong>IP anonymisation</strong> and a <strong>retention window</strong> (default 180 days) are in Settings; old rows prune themselves.</li>
    <li><strong>Bots are flagged, not hidden</strong> &mdash; headline counts exclude them, and the network/hosting
        flag helps you spot crawlers and VPNs.</li>
    <li>The <strong>API fallback</strong> (an online IP lookup) is <em>off by default</em>; turn it on only if you accept
        sending an unresolved IP to a third party (results are cached per IP).</li>
</ul>

<h2 id="cron">Cron &amp; scheduled jobs</h2>
<p>The geo databases refresh on their own on LiteSpeed/PHP-FPM. On plain Apache, or for deterministic timing, run the CLI daily:</p>
<?= $c('30 4 * * * php /path/to/PpPhp/bin/geoip-update.php >> storage/logs/geoip.log 2>&1') ?>
<p>Hosts with no shell can hit the token-guarded web-cron URL instead (the exact URL with its token is shown in the
    console): <code>/cron/geoip?token=&hellip;</code>.</p>

<h2 id="trouble">Troubleshooting</h2>
<ul>
    <li><strong>Everyone shows as &ldquo;Unknown&rdquo; country.</strong> The IP database isn't installed &mdash;
        Settings &rarr; Download / update, or drop an <code>.mmdb</code> into <code>storage/geoip/</code>.</li>
    <li><strong>No organisation / hosting flag.</strong> The network (ASN) database isn't installed &mdash; the same
        download button fetches it too.</li>
    <li><strong>A property isn't appearing.</strong> Check the Site ID matches, the collector URL is reachable from
        the property, and (for browser tracking) that the property is <em>browser-enabled</em>.</li>
    <li><strong>Timezone shows the country but the IP didn't.</strong> That's by design &mdash; the browser's timezone
        fills an unknown location as a last resort, and is marked as source <em>timezone</em>.</li>
    <li><strong>Backups don't show under &ldquo;By server&rdquo;.</strong> A backup only appears once it reports in
        (configure <code>AnalyticsClient</code> on it, or embed the beacon on its pages).</li>
</ul>
