diff --git a/system/public/aesthetic.computer/lib/visit-model.mjs b/system/public/aesthetic.computer/lib/visit-model.mjs index c9b45fadc9..d6c509a866 100644 --- a/system/public/aesthetic.computer/lib/visit-model.mjs +++ b/system/public/aesthetic.computer/lib/visit-model.mjs @@ -34,7 +34,7 @@ export const VISIT_ACTIONS = Object.freeze([ ]); export const ACTIVE_BUCKETS = Object.freeze([0, 10, 30, 60, 180, 600]); export const SURFACES = Object.freeze(["home", "play", "gallery", "read", "support", "other"]); -export const INPUTS = Object.freeze(["pointer", "touch", "keyboard", "gamepad"]); +export const INPUTS = Object.freeze(["pointer", "touch", "keyboard", "scroll", "gamepad"]); export const RETENTION_DAYS = 35; export const VISIT_COLLECTION = "network-visits"; diff --git a/system/public/aesthetic.computer/lib/visit-tracker.mjs b/system/public/aesthetic.computer/lib/visit-tracker.mjs index dccc5906f5..48b5ad48ff 100644 --- a/system/public/aesthetic.computer/lib/visit-tracker.mjs +++ b/system/public/aesthetic.computer/lib/visit-tracker.mjs @@ -86,7 +86,7 @@ export function startVisitTracker(win = window, doc = document) { send(); return true; }; reset(); - for (const [event, input] of [["pointerdown", "pointer"], ["touchstart", "touch"], ["keydown", "keyboard"]]) { + for (const [event, input] of [["pointerdown", "pointer"], ["touchstart", "touch"], ["keydown", "keyboard"], ["wheel", "scroll"]]) { on(win, event, e => { if (!e.isTrusted) return; // Typing in account/contact/editor fields is not collected as interaction. @@ -99,7 +99,7 @@ export function startVisitTracker(win = window, doc = document) { if (!e.isTrusted || e.target?.closest?.("[data-ac-no-track]")) return; const link = e.target?.closest?.("a[href]"); if (!link) return; - interact("pointer"); + interact(e.detail === 0 ? "keyboard" : "pointer"); let url; try { url = new URL(link.href, win.location.href); } catch { return; } if (!/^https?:$/.test(url.protocol)) return; @@ -116,7 +116,9 @@ export function startVisitTracker(win = window, doc = document) { tick(); if (!visible() || !doc.hasFocus()) return; try { - if ([...(nav.getGamepads?.() || [])].some(pad => pad?.buttons?.some(button => button.pressed))) interact("gamepad"); + if ([...(nav.getGamepads?.() || [])].some(pad => + pad?.buttons?.some(button => button.pressed) || + pad?.axes?.slice(0, 4).some(axis => Math.abs(axis) > 0.5))) interact("gamepad"); } catch { /* unavailable in some embedded browsers */ } }, 1000); const api = { action, stop() { stopped = true; win.clearInterval(timer); listeners.forEach(remove => remove()); } }; diff --git a/system/public/menuband/privacy.html b/system/public/menuband/privacy.html index 89d986e123..c851d75a82 100644 --- a/system/public/menuband/privacy.html +++ b/system/public/menuband/privacy.html @@ -148,6 +148,9 @@
Short version: Menu Band has no analytics, advertising, account, or automatic crash reporting.
+The website counts anonymous visits and interaction milestones separately from the Mac app. No form contents or account identity enter those records. See website measurement details.
+Nothing about you. No analytics, no usage telemetry, no automatic crash reporting, no account, and no identifiers.
diff --git a/system/tests/visit-tracking-browser.test.mjs b/system/tests/visit-tracking-browser.test.mjs index 9307edefad..4b4f115386 100644 --- a/system/tests/visit-tracking-browser.test.mjs +++ b/system/tests/visit-tracking-browser.test.mjs @@ -35,6 +35,9 @@ test("browser funnel, automation, privacy opt-out, private SPA routes and duplic await page.locator("button").click(); await page.waitForTimeout(50); assert.ok(received.some(row => row.interacted)); + await page.mouse.wheel(0, 150); + await page.waitForTimeout(50); + assert.ok(received.some(row => row.inputs.includes("scroll")), "reader scrolling counts without storing movement"); await page.clock.runFor(11000); await page.waitForTimeout(50); assert.ok(received.some(row => row.interacted && row.activeSeconds >= 10)); diff --git a/toolchain/analytics/VISITS.md b/toolchain/analytics/VISITS.md index af882fffe4..9444980809 100644 --- a/toolchain/analytics/VISITS.md +++ b/toolchain/analytics/VISITS.md @@ -11,7 +11,7 @@ replays. It does not export operational records to PostHog. | Measure | Definition | | --- | --- | | Visit | One visible top-level page load with a random in-memory ID; returning from a private route starts a fresh visit | -| Interacted visit | Visible-page trusted pointer/touch/keyboard input outside form fields, or a pressed gamepad button | +| Interacted visit | Visible-page trusted pointer/touch/keyboard/wheel input outside form fields, or focused gamepad input (button or stick beyond 0.5) | | Engaged visit | An interacted visit with at least 10 seconds of accumulated visible time | | Action visit | At least one occurrence of a reviewed action during that visit | | Known automation | WebDriver, recognizable bot UA, explicit render query, or `window.acAutomation = true` | @@ -115,11 +115,17 @@ Known deployment boundaries discovered September 23, 2026: - wipppps.world currently serves an external site despite old Lith routing. - aesthetic.direct and digitpain.com did not answer the initial HTTPS probe; local entry sources are prepared, but live coverage is not assumed. +- sotce.net failed this host's TLS probe, but the collector subsequently + received a non-automated visit and interaction from Sotce. Do not interpret + a failed local probe as proof that the property is globally unavailable. The domain allowlist is not a deployment-completion list. Run the coverage audit after shipping; external deployments require their own source/control path. Cloudflare's account inventory and Porkbun's registrar inventory were both consulted; neither alone is a complete list of public web properties. +`visits-deployment.json` preserves the initial front-door audit and the four +properties verified end-to-end through a browser and MongoDB. Measurement +began September 23 at 18:34 UTC; no pre-installation history is invented. ## Verification