I’ve built a Next.js web app (hosted on Vercel, with a Neon Postgres database) that students open on school laptops. When they place those laptops in a charging cart that alternates power banks every 10–15 minutes (and with the tab still on the website), each bank switch briefly “wakes” the browser and triggers a network request to my app’s middleware/DB. Over a full day in the cart, this ends up firing a request every 10 minutes—even though the students aren’t actually using the page—drastically increasing my Neon usage and hitting Vercel unnecessarily.
What I’ve tried so far:
A “visibilitychange + focus” client component in Next.js that increments a counter and redirects after 4 wakes. I added a debouncing window (up to 8 minutes) so that back-to-back visibilitychange and focus events don’t double-count.
Here’s the client component I wrote that is suppose to redirect the user to a separate static webpage hosted on Github pages in order to stop making hits to my Next.js middleware and turning on my Neon database:
// components/AbsentUserChecker.tsx placed in the rootLayout
"use client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
const MAX_VISITS = process.env.NODE_ENV === "development" ? 1000 : 4;
const REDIRECT_URL = "https:www.jotter-blog-still-there/";
// Minimum gap (ms) between two counted wakes.
// If visibilitychange and focus fire within this window, we only count once.
const DEDUPE_WINDOW_MS = 7 * 60 * 1000; // 8 minutes
export default function AbsentUserChecker() {
const pathname = usePathname();
useEffect(() => {
// On mount or when pathname changes, reset if needed:
const storedPath = localStorage.getItem("lastPath");
if (storedPath !== pathname) {
localStorage.setItem("lastPath", pathname);
localStorage.setItem("visitCount", "0");
// Also clear any previous “lastIncrementTS” so we start fresh:
localStorage.setItem("lastIncrementTS", "0");
}
const handleWake = () => {
// Only count if page is actually visible
if (document.visibilityState !== "visible") {
return;
}
const now = Date.now();
// Check the last time we incremented:
const lastInc = parseInt(
localStorage.getItem("lastIncrementTS") || "0",
10
);
if (now - lastInc < DEDUPE_WINDOW_MS) {
// If it’s been less than DEDUPE_WINDOW_MS since the last counted wake,
// abort. This prevents double‐count when visibility+focus fire in quick succession.
return;
}
// Record that we are now counting a new wake at time = now
localStorage.setItem("lastIncrementTS", now.toString());
const storedPath2 = localStorage.getItem("lastPath");
let visitCount = parseInt(
localStorage.getItem("visitCount") || "0",
10
);
// If the user actually navigated to a different URL/pathname, reset to 1
if (storedPath2 !== pathname) {
localStorage.setItem("lastPath", pathname);
localStorage.setItem("visitCount", "1");
return;
}
// Otherwise, same path → increment
visitCount += 1;
localStorage.setItem("visitCount", visitCount.toString());
// If we reach MAX_VISITS, clear and redirect
if (visitCount >= MAX_VISITS) {
localStorage.removeItem("visitCount");
localStorage.removeItem("lastPath");
localStorage.removeItem("lastIncrementTS");
window.location.href = REDIRECT_URL;
}
};
document.addEventListener("visibilitychange", handleWake);
window.addEventListener("focus", handleWake);
return () => {
document.removeEventListener("visibilitychange", handleWake);
window.removeEventListener("focus", handleWake);
};
}, [pathname]);
return null;
}
The core issue:
Charging-cart bank switches either (a) don’t toggle visibilityState in some OS/browser combos, or (b) fully freeze/suspend the tab with no “resume” event until a human opens the lid. As a result, my client logic never sees a “wake” event—and so the counter never increments and no redirect happens. Meanwhile, the cart’s brief power fluctuation still wakes the network layer enough to hit my server.
What I’m looking for:
Is there any reliable, cross-browser event or API left that will fire when a laptop’s power source changes (AC ↔ battery) or when the OS briefly re-enables the network—even if the tab never “becomes visible” or “gains focus”? If not, what other strategies can I use to prevent these phantom hits without accidentally logging students out or redirecting them when they’re legitimately interacting? Any ideas or workarounds would be hugely appreciated!
