From 5daf221cd7c6405ae6358e212f151a8af5eb1c85 Mon Sep 17 00:00:00 2001 From: pleb Date: Wed, 29 Oct 2025 15:56:42 -0700 Subject: [PATCH] Must be ranked higher than pleb to use tools --- AGENTS.md | 18 ++ src/lib/components/HasToolAccess.svelte | 21 +- src/lib/components/NavBar.svelte | 41 +++- src/lib/server/sessionStore.ts | 5 + src/lib/utils/plebsaber-utils.ts | 45 +++- src/routes/api/beatleader/me/+server.ts | 35 +++- src/routes/tools/+layout.server.ts | 22 +- .../tools/beatleader-compare/+page.svelte | 5 +- .../tools/beatleader-headtohead/+page.svelte | 5 +- .../beatleader-playlist-gap/+page.svelte | 5 +- src/routes/tools/stats/+page.server.ts | 46 +++++ src/routes/tools/stats/+page.svelte | 195 ++++++++++++++++++ 12 files changed, 420 insertions(+), 23 deletions(-) create mode 100644 AGENTS.md create mode 100644 src/routes/tools/stats/+page.server.ts create mode 100644 src/routes/tools/stats/+page.svelte diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..1dbe858 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# AGENT NOTES + +- SvelteKit app under `src/`; tools require BeatLeader auth and rank gating. +- Shared logic lives in `src/lib/utils/plebsaber-utils.ts` (requirements, playlist helpers, etc.). +- Tool routes (`/tools/*`) use a layout that fetches the BeatLeader profile once (`+layout.server.ts`). +- UI components for gating (e.g., `HasToolAccess.svelte`) handle supporter/top-3k restrictions. +- BeatLeader session info exposed via `/api/beatleader/me`; navbar warns if outside allowed rank. +- OAuth callback stores identity in a long-lived session (`plebsaber_session` via `.data/plebsaber_sessions.json`). +- Tools expect BeatLeader supporter or global rank better than pleb's current rank; navbar dropdown warns when outside that threshold. +- Global navigation lives in `src/lib/components/NavBar.svelte`; it houses the BeatLeader login dropdown with profile, testing (dev) and logout actions. +- Core BeatLeader tool pages reuse shared components such as `PlayerCompareForm.svelte`, `MapCard.svelte`, and `SongPlayer.svelte` for consistent UI/UX. +- `/testing` route (dev-only) consumes `/api/beatleader/me` to display and log raw BeatLeader session data for debugging. +- `/tools/stats` (admin-only, BL id `76561199407393962`) lists every stored BeatLeader session with avatars and last-seen timestamps; navigation link only appears for that account. + +## Shell command guidance + +- Whitelisted commands: `grep` +- DO NOT USE: `cd` (prefer `pwd`) diff --git a/src/lib/components/HasToolAccess.svelte b/src/lib/components/HasToolAccess.svelte index 378acbc..072925e 100644 --- a/src/lib/components/HasToolAccess.svelte +++ b/src/lib/components/HasToolAccess.svelte @@ -2,6 +2,7 @@ import { formatToolRequirementSummary, meetsToolRequirement, + DEFAULT_ADMIN_RANK_FALLBACK, type BeatLeaderPlayerProfile, type ToolRequirement } from '$lib/utils/plebsaber-utils'; @@ -12,10 +13,22 @@ export let requirement: ToolRequirement | null = null; export let customLockedMessage: string | null = null; export let showCurrentRank = true; + export let adminRank: number | null = null; - $: hasAccess = meetsToolRequirement(player, requirement); - $: summary = formatToolRequirementSummary(requirement); - $: lockedMessage = customLockedMessage ?? requirement?.lockedMessage ?? null; + $: requirementContext = { adminRank }; + $: hasAccess = meetsToolRequirement(player, requirement, requirementContext); + $: summary = formatToolRequirementSummary(requirement, requirementContext); + $: fallbackBaseline = DEFAULT_ADMIN_RANK_FALLBACK; + $: resolvedBaseline = typeof adminRank === 'number' && Number.isFinite(adminRank) && adminRank > 0 + ? adminRank + : fallbackBaseline; + $: baselineCopy = resolvedBaseline + ? `players ranked better than pleb (#${resolvedBaseline.toLocaleString()})` + : 'players ranked better than pleb'; + $: defaultLockedMessage = requirement?.requiresBetterRankThanAdmin + ? `You must be a BL Patreon supporter or ${baselineCopy} to use this tool.` + : requirement?.lockedMessage ?? null; + $: lockedMessage = customLockedMessage ?? defaultLockedMessage; $: showLockedMessage = lockedMessage && lockedMessage !== summary ? lockedMessage : null; $: playerRank = typeof player?.rank === 'number' ? player?.rank ?? null : null; $: playerRankDisplay = playerRank !== null ? `#${playerRank.toLocaleString()}` : null; @@ -26,7 +39,7 @@ {:else}

- Tools are restricted to BeatLeader supporters (and the top 3k ranked players). + Tools are restricted to BeatLeader supporters (and {baselineCopy}).

{#if summary}

{summary}

diff --git a/src/lib/components/NavBar.svelte b/src/lib/components/NavBar.svelte index 8efbfb1..77d4d5e 100644 --- a/src/lib/components/NavBar.svelte +++ b/src/lib/components/NavBar.svelte @@ -2,7 +2,7 @@ import { onMount } from 'svelte'; import { dev } from '$app/environment'; import beatleaderLogo from '$lib/assets/beatleader-logo.png'; - + import { DEFAULT_ADMIN_RANK_FALLBACK, PLEB_BEATLEADER_ID } from '$lib/utils/plebsaber-utils'; const links = [ { href: '/', label: 'Home' }, { href: '/tools', label: 'Tools' }, @@ -37,6 +37,8 @@ let loginHref = '/auth/beatleader/login'; let checkingSession = true; let menuOpen = false; + let adminRank: number | null = null; + let adminFallbackRank = DEFAULT_ADMIN_RANK_FALLBACK; const getProfileUrl = (id?: string) => (id ? `https://beatleader.com/u/${encodeURIComponent(id)}` : 'https://beatleader.com'); @@ -64,6 +66,18 @@ }; } + function extractAdminBaseline(payload: unknown): { rank: number | null; fallback: number } | null { + if (!payload || typeof payload !== 'object') return null; + const baseline = (payload as { adminBaseline?: { rank?: unknown; fallback?: unknown } }).adminBaseline; + if (!baseline || typeof baseline !== 'object') return null; + const rank = (baseline as { rank?: unknown }).rank; + const fallback = (baseline as { fallback?: unknown }).fallback; + return { + rank: typeof rank === 'number' && Number.isFinite(rank) && rank > 0 ? rank : null, + fallback: typeof fallback === 'number' && Number.isFinite(fallback) && fallback > 0 ? fallback : DEFAULT_ADMIN_RANK_FALLBACK + }; + } + onMount(() => { const redirectTarget = `${window.location.pathname}${window.location.search}${window.location.hash}` || '/'; loginHref = `/auth/beatleader/login?redirect_uri=${encodeURIComponent(redirectTarget)}`; @@ -77,6 +91,11 @@ if (profile) { user = profile; } + const baseline = extractAdminBaseline(json); + if (baseline) { + adminRank = baseline.rank; + adminFallbackRank = baseline.fallback; + } } else if (res.status === 401) { try { const body = (await res.json()) as Record; @@ -103,6 +122,12 @@ function closeMenu() { menuOpen = false; } + + $: baselineRank = (adminRank ?? adminFallbackRank ?? DEFAULT_ADMIN_RANK_FALLBACK); + $: requiresWarning = typeof user?.rank === 'number' && Number.isFinite(user.rank) + ? (user.rank as number) > baselineRank + : false; + $: baselineCopy = baselineRank ? `pleb (#${baselineRank.toLocaleString()})` : 'pleb';
@@ -142,12 +167,15 @@ {#if menuOpen}