Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/src/app/api/posts/reported/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,10 @@ export const dynamic = "force-dynamic";
export async function GET(req: Request) {
const w = (new URL(req.url).searchParams.get("wallet") ?? "").toLowerCase();
if (!adminWallets().has(w)) return NextResponse.json({ error: "admin only" }, { status: 403 });
return NextResponse.json({ posts: await listReported() }, { headers: { "cache-control": "no-store" } });
try {
return NextResponse.json({ posts: await listReported() }, { headers: { "cache-control": "no-store" } });
} catch (err) {
console.error("[posts] reported failed:", err instanceof Error ? err.message : err);
return NextResponse.json({ error: "could not load reported posts" }, { status: 502, headers: { "cache-control": "no-store" } });
}
}
29 changes: 22 additions & 7 deletions app/src/app/api/presence/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,38 @@ export const dynamic = "force-dynamic";

/** GET → {visits, online}. POST (browser beacon) → records presence, returns the same. No cookies, no PII stored. */
export async function GET() {
return NextResponse.json(await memo("pulse", 2_000, () => readPulse()), { headers: { "cache-control": "no-store" } });
try {
return NextResponse.json(await memo("pulse", 2_000, () => readPulse()), { headers: { "cache-control": "no-store" } });
} catch (err) {
console.error("[presence] pulse failed:", err instanceof Error ? err.message : err);
return NextResponse.json({ error: "could not load presence" }, { status: 502, headers: { "cache-control": "no-store" } });
}
}

let lastPrune = 0;

export async function POST(req: Request) {
const ua = req.headers.get("user-agent") ?? "";
if (looksLikeBot(ua)) return NextResponse.json(await readPulse(), { headers: { "cache-control": "no-store" } });
try {
if (looksLikeBot(ua)) return NextResponse.json(await readPulse(), { headers: { "cache-control": "no-store" } });
} catch (err) {
console.error("[presence] pulse failed:", err instanceof Error ? err.message : err);
return NextResponse.json({ error: "could not load presence" }, { status: 502, headers: { "cache-control": "no-store" } });
}
const ip = (req.headers.get("fly-client-ip") || req.headers.get("x-forwarded-for") || "").split(",")[0].trim() || "0.0.0.0";
// The visitor hash mixes in the UA, which the caller fully controls: without
// a bucket, rotating UAs mints a fresh bb_presence row per request and
// inflates visits. 30/min leaves normal beaconing (~every few seconds) alone.
if (rateLimited(`presence:ip:${ip}`, 30)) return NextResponse.json({ error: "slow down" }, { status: 429 });
const pulse = await recordBeacon(visitorHash(ip, ua));
if (Date.now() - lastPrune > 3_600_000) {
lastPrune = Date.now();
void prunePresence().catch(() => {});
try {
const pulse = await recordBeacon(visitorHash(ip, ua));
if (Date.now() - lastPrune > 3_600_000) {
lastPrune = Date.now();
void prunePresence().catch(() => {});
}
return NextResponse.json(pulse, { headers: { "cache-control": "no-store" } });
} catch (err) {
console.error("[presence] beacon failed:", err instanceof Error ? err.message : err);
return NextResponse.json({ error: "could not record presence" }, { status: 502, headers: { "cache-control": "no-store" } });
}
return NextResponse.json(pulse, { headers: { "cache-control": "no-store" } });
}
24 changes: 17 additions & 7 deletions app/src/components/launchpad/AdminQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,25 @@ export default function AdminQueue() {
const [err, setErr] = useState<string | null>(null);
const load = useCallback(async () => {
if (!address) return;
const res = await fetch(`/api/posts/reported?wallet=${address}`, { cache: "no-store" });
const d = (await res.json()) as { posts?: PostRow[]; error?: string };
if (!res.ok) {
setErr(d.error ?? "not allowed");
try {
const res = await fetch(`/api/posts/reported?wallet=${address}`, { cache: "no-store" });
let d: { posts?: PostRow[]; error?: string };
try {
d = (await res.json()) as { posts?: PostRow[]; error?: string };
} catch {
throw new Error(`moderation queue returned ${res.status}`);
}
if (!res.ok) {
setErr(d.error ?? "not allowed");
setPosts(null);
return;
}
setErr(null);
setPosts(d.posts ?? []);
} catch (e) {
setErr(friendlyError(e));
setPosts(null);
return;
}
setErr(null);
setPosts(d.posts ?? []);
}, [address]);
useEffect(() => {
const id = setTimeout(() => void load(), 0);
Expand Down
3 changes: 3 additions & 0 deletions app/src/lib/launchpad/holders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ test("shareBps / fmtShare", () => {
assert.equal(fmtShare(42), "0.42%");
assert.equal(fmtShare(0), "0%");
assert.equal(fmtShare(0.5), "<0.01%");
assert.equal(fmtShare(NaN), "—", "non-finite never paints NaN% in the trust panel");
assert.equal(fmtShare(Infinity), "—");
assert.equal(fmtShare(-Infinity), "—");
});

test("holderTags: creator, pool, burn, sniper, whale (whale never on pool/burn)", () => {
Expand Down
1 change: 1 addition & 0 deletions app/src/lib/launchpad/holders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function shareBps(balance: bigint, supply: bigint): number {
}

export function fmtShare(bps: number): string {
if (!Number.isFinite(bps)) return "—";
if (bps <= 0) return "0%";
if (bps < 1) return "<0.01%";
const pct = bps / 100;
Expand Down
19 changes: 19 additions & 0 deletions app/src/lib/launchpad/presence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { looksLikeBot, visitorHash } from "./presence.ts";

test("bot beacons skip the write path without minting visits", () => {
for (const ua of ["Googlebot/2.1", "curl/8.0", "python-requests/2.31", "axios/1.0", "Lighthouse"]) assert.equal(looksLikeBot(ua), true, ua);
assert.equal(looksLikeBot("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"), false);
assert.equal(looksLikeBot(""), false);
});

test("visitor hashes are opaque 64-hex and differ per visitor", () => {
const a = visitorHash("1.2.3.4", "Mozilla/5.0");
const b = visitorHash("5.6.7.8", "Mozilla/5.0");
const c = visitorHash("1.2.3.4", "curl/8.0");
for (const h of [a, b, c]) assert.match(h, /^[0-9a-f]{64}$/);
assert.notEqual(a, b, "different IPs differ");
assert.notEqual(a, c, "different UAs differ");
assert.equal(a, visitorHash("1.2.3.4", "Mozilla/5.0"), "stable within the day");
});
Loading