Conversation
Add an AdGatekeeper that permanently suppresses the intrusive in-game ad
for any user ever detected running an adblocker. Ad-block users are highly
ad-sensitive, so the verdict is terminal and persisted to
localStorage("adblock-detected") — disabling the blocker does not unlock the
ad, in this or any future session. Detection uses a DOM bait probe plus, when
available, Admiral's measure.detected signal (adblocking && !whitelisted) as a
faster, more reliable read. Clean users are never latched (no false positives).
Add Admiral.ts, which injects the ad-recovery tag (command-queue stub +
payload + GAM targeting shim) for ad-eligible users only. Paid/adfree users
have window.adsEnabled === false, so Admiral never loads and its adblock
popup can never fire for them.
Both are wired into the existing window.adsEnabled / userMeResponse flow in
Main.ts; the in-game ad (InGamePromo) now loads via adGatekeeper.whenClear.
Passive homepage/gutter ads are unchanged.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
WalkthroughThis PR adds a client-side ChangesAdblock Gate and Admiral Integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant Client as Main.ts
participant Admiral
participant AdGatekeeper
participant InGamePromo
Client->>Admiral: loadAdmiral()
Admiral-->>Client: onAdmiralMeasured(result)
Client->>AdGatekeeper: seed(adblocking)
Client->>AdGatekeeper: start()
AdGatekeeper->>AdGatekeeper: probe / check localStorage
InGamePromo->>AdGatekeeper: whenClear(showAd callback)
AdGatekeeper-->>InGamePromo: fires only when state is "clear"
InGamePromo->>InGamePromo: loadAd() / checkForAds()
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client/AdGatekeeper.ts`:
- Around line 90-106: The first probe in AdGatekeeper.start() runs too early and
can clear the gate before Admiral’s initial reading arrives. Update the
start()/evaluate()/transition flow so the initial DOM bait is deferred until the
first Admiral result is available, or add a brief grace period before
transitioning to "clear". Make sure whenClear() in InGamePromo cannot fire until
that first authoritative Admiral state has been seen.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 2bdb24a6-14f8-4926-bc38-1355e3a24fb9
📒 Files selected for processing (5)
src/client/AdGatekeeper.tssrc/client/Admiral.tssrc/client/Main.tssrc/client/hud/layers/InGamePromo.tstests/AdGatekeeper.test.ts
| start(): void { | ||
| if (this.started) return; | ||
| this.started = true; | ||
| // A blocker detected in any past session suppresses the ad forever — no | ||
| // need to probe or listen at all. | ||
| if (readPersistedBlock()) { | ||
| this.transition("blocked"); | ||
| return; | ||
| } | ||
| void this.evaluate(); | ||
| // Toggling an extension means leaving the tab and coming back — re-check on | ||
| // return. Cheap, event-driven, and catches a mid-session enable before the | ||
| // in-game ad fires, without hammering a poll. | ||
| document.addEventListener("visibilitychange", this.onVisibility); | ||
| window.addEventListener("focus", this.onFocus); | ||
| this.pollTimer = setInterval(() => void this.evaluate(), this.pollMs); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
git ls-files src/client/AdGatekeeper.ts src/client/Main.ts src/client | sed -n '1,120p'Repository: openfrontio/OpenFrontIO
Length of output: 4636
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in src/client/AdGatekeeper.ts src/client/Main.ts src/client/Admiral.ts; do
echo "### $f"
wc -l "$f"
echo
cat -n "$f" | sed -n '1,260p'
echo
doneRepository: openfrontio/OpenFrontIO
Length of output: 23061
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "adGatekeeper|onAdmiralMeasured|loadAdmiral|whenClear|seed\\(" src/client/Main.ts src/client/AdGatekeeper.ts src/client/Admiral.ts
echo
cat -n src/client/Main.ts | sed -n '520,760p'Repository: openfrontio/OpenFrontIO
Length of output: 10096
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n "whenClear\\(" src/client/Main.ts src/client -g '!src/client/AdGatekeeper.ts'Repository: openfrontio/OpenFrontIO
Length of output: 253
🏁 Script executed:
#!/bin/bash
set -euo pipefail
wc -l src/client/hud/layers/InGamePromo.ts
echo
cat -n src/client/hud/layers/InGamePromo.ts | sed -n '1,180p'Repository: openfrontio/OpenFrontIO
Length of output: 6531
Delay the first probe until Admiral can seed the gate
start() runs the DOM bait immediately, before Admiral has a chance to report. If the bait misses a blocker but Admiral later returns adblocking && !whitelisted, whenClear() in InGamePromo has already fired and the ad load is scheduled, so blocker users can still see the in-game ad. Skip the first evaluate() until the first Admiral reading lands, or add a short grace window before clearing.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/client/AdGatekeeper.ts` around lines 90 - 106, The first probe in
AdGatekeeper.start() runs too early and can clear the gate before Admiral’s
initial reading arrives. Update the start()/evaluate()/transition flow so the
initial DOM bait is deferred until the first Admiral result is available, or add
a brief grace period before transitioning to "clear". Make sure whenClear() in
InGamePromo cannot fire until that first authoritative Admiral state has been
seen.
What
Two related pieces, wired into the existing
window.adsEnabled/userMeResponsead flow:AdGatekeeper— decides whether the intrusive in-game ad may show. Once a blocker is ever detected, the ad is suppressed permanently (terminal state, persisted tolocalStorage["adblock-detected"]). Ad-block users are highly ad-sensitive, so disabling the blocker does not unlock the ad — in this or any future session. Detection = a DOM bait probe, refined by Admiral'smeasure.detectedsignal (adblocking && !whitelisted) when it fires. Clean users are never latched.Admiral.ts— injects the ad-recovery tag (command-queue stub + payload + GAM targeting shim) for ad-eligible users only. Paid/adfreeusers havewindow.adsEnabled === false, so Admiral never loads and its adblock popup can't fire for them.Only the in-game ad (
InGamePromo) is gated — it now loads viaadGatekeeper.whenClear(...). Passive homepage/gutter ads are unchanged.Why
adfreefor life) must never see ads or load Admiral.How it behaves
adfree)Testing
tests/AdGatekeeper.test.ts(9 cases) — terminal latch, "disabling blocker doesn't unlock", cross-session persistence, seed path, no-false-positive.tscclean,eslintclean.adsEnabled: true, Admiral tag injected + payload initialized,persisted: null(no false positive); simulated blocker → flag latches to"1"; reload with no blocker → still"1"(forever); reset clean afterward.Notes / follow-ups
ADMIRAL_PAYLOAD_SRCis a disguised, rotating domain — re-sync from the provider when they reissue the tag.res.subscribed(Admiral's own ad-free pass) is intentionally ignored — OpenFront's ad-free is the serveradfreeflag.🤖 Generated with Claude Code