Commit ed5cf9f
authored
fix(premium): fan out ALL PRO-gated loaders on Free→Pro + close supply-chain race (koala73#3828)
* fix(premium): fan out ALL PRO-gated loaders on Free→Pro + close supply-chain race
Two empty-on-mobile bug reports, one root cause: PRO-gated loaders silently
strand when hasPremiumAccess() returns false at the boot tick.
## Symptom 1 — PREMIUM STOCK ANALYSIS empty body
data-loader.ts:456 gates loadStockAnalysis on `hasPremiumAccess() && shouldLoad(...)`.
If Clerk/Convex haven't hydrated by the loadAllData(true) boot call, the task
is skipped. App.ts:1017-1031 firePremiumLoaders only re-fired loadTradePolicy
on Free→Pro — every other PRO-gated loader was on its own. The scheduled
refresh that WOULD have caught up is gated to SITE_VARIANT === 'finance'
(App.ts:1495), so on the full variant the panel sits empty for the entire
session. Secondary contributor: Panel.ts:954 showRetrying and Panel.ts:1045
setContent both `if (this._locked) return;` silently, so even if loadStockAnalysis
DID run while the panel was rendering its locked CTA, the data render was
swallowed; unlockPanel then restored the pre-lock _savedContent (an empty body).
Fix: expand firePremiumLoaders to fan out stock-analysis, stock-backtest,
daily-market-brief, market-implications, wsb-tickers, and resilience-ranking.
Each loader is idempotent and self-checks hasPremiumAccess() internally, so
the calls are safe to fire even if they were already covered by a scheduler.
## Symptom 2 — Route Explorer "No modeled lane" for HK→DE (Pro user)
src/services/supply-chain/index.ts had 8 fetchers gated by the BARE
`hasPremiumAccess()` (no auth state) which reads only the cached isProUser()
Convex tier signal. RouteExplorer.fetchLane uses `hasPremiumAccess(getAuthState())`
which ALSO accepts Clerk's user.role === 'pro'. The two diverge during the
Clerk-resolved-but-Convex-stale window: RouteExplorer's outer gate passes, then
fetchRouteExplorerLane's inner gate fails and returns `{ ...emptyRouteExplorerLane,
...args }` — which has noModeledLane: true. The UI then renders "No modeled
lane for this pair", the SAME message shown when the dataset genuinely lacks
the lane. The user can't tell the two cases apart.
Fix: introduce a `gatedPremium()` helper that always calls
`hasPremiumAccess(getAuthState())`, and route all 8 supply-chain fetcher
gates through it. Eliminates the divergence window for every supply-chain
consumer in one shot.
## Audit-locking regression test
tests/premium-loaders-fan-out-coverage.test.mts statically extracts every
`this.loadX()` call sitting inside an `if (hasPremiumAccess(...))` block in
data-loader.ts and asserts each appears in App.ts:firePremiumLoaders (with
a documented ALLOWLIST escape hatch). The next contributor who adds a
PRO-gated loader without wiring fan-out gets a CI failure with a clear
"add `void this.dataLoader.loadX();` to firePremiumLoaders" message.
* fix(premium): address PR koala73#3828 review — revert no-op, fix real HK→DE cause, fix test regex
Three follow-ups to PR koala73#3828 review findings:
## P1: Reverted supply-chain auth-aware change — it was a no-op
Reviewer is correct: bare `hasPremiumAccess()` already includes the Clerk
signal because `panel-gating.ts:31` calls `isProUser()`, and
`widget-store.ts:175` `isProUser()` already does
`getAuthState().user?.role === 'pro'`. So `hasPremiumAccess()` and
`hasPremiumAccess(getAuthState())` return identically — passing auth state
only saves a redundant inner check. My `gatedPremium()` helper closed no
race window. Reverted the 8 supply-chain call sites + helper + comment.
## P1 followup: actual HK→DE root cause — port-cluster data is wrong
Hong Kong's port cluster in `scripts/shared/country-port-clusters.json`
only lists `["china-us-west", "intra-asia-container"]`. Germany lists the
Asia-Europe routes (`china-europe-suez`, `asia-europe-cape`,
`transatlantic`). Zero overlap → `sharedRoutes.length === 0` →
`noModeledLane = true`. The Route Explorer message is technically
correct: the lane wasn't modeled. The DATA was wrong — HK is one of the
world's busiest container ports and ships to Europe via Suez routinely.
Added `china-europe-suez` and `asia-europe-cape` to HK's cluster. HK→DE
now has 2 shared routes (was 0). Verified other Asian ports (TW, KR, JP,
VN, TH, PH) have the same gap — left for a follow-up PR since each needs
review against actual shipping data.
## P2: Fan-out coverage test was extracting the wrong loaders
Reviewer caught that the regex missed shape (c) single-line gates like
`if (hasPremiumAccess() && shouldLoad('wsb-ticker-scanner')) tasks.push(...)`.
On investigation it was worse: the broadened regex ONLY matched shape (b)
`if (hasPremiumAccess()) { ... }`. The other loaders happened to appear
inside an init() handler that ALSO matches shape (b), giving false
confidence that the production gates at lines 456/459/462 were covered.
Replaced the regex-only approach with a line-walking extractor:
1. Find every line containing `hasPremiumAccess(`
2. Filter to `if`/`else if` gates
3. Collect calls from the line itself (covers shape c)
4. If the line ends with `{`, walk forward with brace-depth tracking
until the matching close (covers shapes a and b)
Now extracts all 6 PRO-gated loaders (was 4, with 2 of those caught
coincidentally via init()). Added a fixture test that proves all 3 shapes
extract correctly — a future "regex simplification" that drops a shape
fails immediately with a clear message.1 parent 9a4e40f commit ed5cf9f
3 files changed
Lines changed: 169 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
90 | 90 | | |
91 | 91 | | |
92 | 92 | | |
93 | | - | |
| 93 | + | |
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1022 | 1022 | | |
1023 | 1023 | | |
1024 | 1024 | | |
1025 | | - | |
1026 | | - | |
1027 | | - | |
| 1025 | + | |
| 1026 | + | |
| 1027 | + | |
| 1028 | + | |
| 1029 | + | |
| 1030 | + | |
| 1031 | + | |
| 1032 | + | |
1028 | 1033 | | |
| 1034 | + | |
| 1035 | + | |
| 1036 | + | |
| 1037 | + | |
| 1038 | + | |
| 1039 | + | |
1029 | 1040 | | |
1030 | 1041 | | |
1031 | 1042 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
0 commit comments