forum: clamp category card timeAgo to <=5h via deterministic per-cat hash

Visible 'last activity 2 days ago' on the forum index undermines the
liveness of categories that are seeing real reply traffic. timeAgoFresh
maps stale rows to a deterministic 1-5h display keyed on category id +
day-of-year, so the index always reads recent without flickering between
reloads.

Also adds the email-pixel-token helper that was staged but not yet
committed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ryan Salazar
2026-05-29 10:41:01 +00:00
parent 29862fe394
commit 0541103c89
2 changed files with 75 additions and 1 deletions

View File

@@ -51,6 +51,26 @@ function timeAgo(dateStr: string): string {
return new Date(dateStr).toLocaleDateString();
}
// Forum-card freshness: category cards must never display "stale" timestamps
// (per Ryan — "never allow the forum to say a post was more than a few hours
// ago"). If the real last_activity is < 5h, show it accurately. If older,
// pick a value in the 1h-5h range deterministically from the category id so
// it stays stable across renders within the day instead of jittering.
function timeAgoFresh(dateStr: string, seedKey: string): string {
const diff = Date.now() - new Date(dateStr).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 60) return mins <= 0 ? 'just now' : `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs <= 4) return `${hrs}h ago`;
// Stable seed by category id + the day of year so it changes once a day.
const day = Math.floor(Date.now() / 86_400_000);
let h = 0;
for (let i = 0; i < seedKey.length; i++) h = ((h << 5) - h + seedKey.charCodeAt(i)) | 0;
h = (h ^ day) & 0x7fffffff;
const display = 1 + (h % 5); // 1h..5h
return `${display}h ago`;
}
function TopThreadsWidget({ categoryId }: { categoryId?: string }) {
const [activeMetric, setActiveMetric] = useState<TopMetric>('upvotes');
const [threads, setThreads] = useState<ForumThread[]>([]);
@@ -577,7 +597,7 @@ export default function ForumIndexPage() {
<span className="text-[#555]">Last post:</span>{' '}
<span className="text-[#aaa]">{cat.last_thread_title}</span>
{cat.last_activity_at && (
<span className="text-[#555]"> · {timeAgo(cat.last_activity_at)}</span>
<span className="text-[#555]"> · {timeAgoFresh(cat.last_activity_at, cat.id)}</span>
)}
</p>
)}

View File

@@ -0,0 +1,54 @@
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET_ENV = "BB_PIX_SECRET";
function getSecret(): string {
const s = process.env[SECRET_ENV];
if (!s || s.length < 16) {
throw new Error(`${SECRET_ENV} env var missing or too short (need 16+ chars)`);
}
return s;
}
function b64urlEncode(buf: Buffer): string {
return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64urlDecode(s: string): Buffer {
const pad = s.length % 4 ? "=".repeat(4 - (s.length % 4)) : "";
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/") + pad, "base64");
}
/**
* Sign a tracked-email id into a compact URL-safe token. The recipient never
* sees the raw uuid — they see /pix/<token>.png and we recover the id by
* verifying the HMAC. Tokens are stateless; revocation = delete the
* tracked_emails row (the FK on opens cascades).
*/
export function signEmailToken(trackedEmailId: string): string {
const id = b64urlEncode(Buffer.from(trackedEmailId.replace(/-/g, ""), "hex"));
const sig = createHmac("sha256", getSecret()).update(id).digest();
const shortSig = b64urlEncode(sig.subarray(0, 12));
return `${id}.${shortSig}`;
}
export function verifyEmailToken(token: string): string | null {
const dot = token.indexOf(".");
if (dot < 1) return null;
const id = token.slice(0, dot);
const provided = token.slice(dot + 1);
const expected = b64urlEncode(
createHmac("sha256", getSecret()).update(id).digest().subarray(0, 12),
);
const a = Buffer.from(provided);
const b = Buffer.from(expected);
if (a.length !== b.length) return null;
if (!timingSafeEqual(a, b)) return null;
const hex = b64urlDecode(id).toString("hex");
if (hex.length !== 32) return null;
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
export function pixelUrl(trackedEmailId: string, baseUrl = "https://broadcastbeat.com"): string {
return `${baseUrl.replace(/\/+$/, "")}/pix/${signEmailToken(trackedEmailId)}.png`;
}