ReviewOS

also looking at this

stacks/lastsoulultra

LSU's own coaching surface, and what auditing the site turned up

#12
Merged glennmichael123 wants to merge claude/last-soul-ultra-web-6e0718 into main
86 files +3943 -1190

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

public/assets/styles/production/coaching.cssmodified+2-0
Changes to public/assets/styles/production/coaching.css
@@ -88,6 +88,8 @@
8888.coaching-surface .coach-empty {margin:0 0 clamp(40px,5vw,72px);padding:clamp(30px,5vw,56px);border:var(--coach-rule);background:var(--lsu-card)}
8989.coaching-surface .coach-empty h2 {margin:0;font:400 clamp(24px,3vw,34px)/1.1 var(--lsu-display);text-transform:uppercase}
9090.coaching-surface .coach-empty p {margin:16px 0 0;max-width:58ch;color:var(--lsu-dim-1);font:400 16px/1.7 var(--lsu-body)}
91.coaching-surface .coach-empty-back a {color:var(--lsu-white);text-decoration:none;border-bottom:1px solid var(--lsu-red);padding-bottom:2px}
92.coaching-surface .coach-empty-back a:hover {color:var(--lsu-red)}
9193
9294/* Focus has to clear the black. A red ring on #000 is not reliably visible at
9395 small sizes, so the ring is white with a red cast behind it. */
resources/assets/styles/site/coaching.cssmodified+2-0
Changes to resources/assets/styles/site/coaching.css
@@ -88,6 +88,8 @@
8888.coaching-surface .coach-empty {margin:0 0 clamp(40px,5vw,72px);padding:clamp(30px,5vw,56px);border:var(--coach-rule);background:var(--lsu-card)}
8989.coaching-surface .coach-empty h2 {margin:0;font:400 clamp(24px,3vw,34px)/1.1 var(--lsu-display);text-transform:uppercase}
9090.coaching-surface .coach-empty p {margin:16px 0 0;max-width:58ch;color:var(--lsu-dim-1);font:400 16px/1.7 var(--lsu-body)}
91.coaching-surface .coach-empty-back a {color:var(--lsu-white);text-decoration:none;border-bottom:1px solid var(--lsu-red);padding-bottom:2px}
92.coaching-surface .coach-empty-back a:hover {color:var(--lsu-red)}
9193
9294/* Focus has to clear the black. A red ring on #000 is not reliably visible at
9395 small sizes, so the ring is white with a red cast behind it. */
resources/components/CoachingModule.stxmodified+21-4
Changes to resources/components/CoachingModule.stx
@@ -4,13 +4,22 @@ const { slug } = defineProps<Props>()
44</script>
55<script server>
66import { coachingModule } from '../data/coaching'
7import { portalHost, portalUrl } from '../functions/coaching-source'
7import { loadBrand, portalHost, portalUrl } from '../functions/coaching-source'
88
99/* The registry is the only place a module's words live, so a page cannot
1010 disagree with the nav about what it is called. */
1111const panel = coachingModule(slug)
1212if (!panel) throw new Error(`unknown coaching module: ${slug}`)
1313
14/* Whether the brand has published this module, read from the brand itself
15 rather than from what the nav chooses to show. Without it the page framed
16 a panel that answers 404, under a notice promising an account would open
17 it. The storefront keeps its own empty state, so it is excluded here
18 rather than carrying two. */
19const panelBrand = await loadBrand()
20const panelLive = Boolean(panelBrand?.modules.includes(panel.slug))
21const panelStorefront = panel.slug === 'plans'
22
1423/* The panel is a cross-origin frame, and a frame can decline to load for
1524 reasons this page cannot see: an origin the brand has not allowed, a
1625 browser blocking third-party frames, an extension. When that happens the
@@ -28,20 +37,27 @@ const panelHost = portalHost()
2837 </header>
2938 <CoachingNav current="" />
3039 <div class="coach-shell">
31 @if(panel.scope === 'private')
40 @if(panelLive && panel.scope === 'private')
3241 <div class="coach-scope">
3342 <strong>Only you see this</strong>
3443 <p>This panel reads your own training account, not anything this site holds. Last Soul Ultra cannot see it, and neither can the coach whose method you are following.</p>
3544 </div>
36 @elseif(panel.scope === 'athlete')
45 @elseif(panelLive && panel.scope === 'athlete')
3746 <div class="coach-scope">
3847 <strong>Needs a training account</strong>
3948 <p>Sign in happens inside the panel, with the account that holds your subscription. That is a separate account from the one you use to request a race entry.</p>
4049 </div>
4150 @endif
42 @if(panel.slug === 'plans')
51 @if(panelStorefront)
4352 <CoachingOffers />
53 @elseif(!panelLive)
54 <div class="coach-empty">
55 <h2>Not open yet</h2>
56 <p>No coach has published a method here, so there is nothing for this panel to show. It fills in once the first tier opens, under the coach's own name and the coach's own price.</p>
57 <p class="coach-empty-back"><a href="/coaching">Read what coaching here will be</a></p>
58 </div>
4459 @endif
60 @if(panelLive)
4561 <div class="coach-panel-bar">
4662 <p>Loaded from {{ panelHost }}. Your training account lives there, not here.</p>
4763 <a href="" target="_blank" rel="noopener">Open it there<span class="coach-sr"> (opens in a new tab)</span></a>
@@ -49,5 +65,6 @@ const panelHost = portalHost()
4965 <div class="coach-panel">
5066 <CoachingPortal section="" />
5167 </div>
68 @endif
5269 </div>
5370</div>
tests/coaching-views.test.tsmodified+30-1
Changes to tests/coaching-views.test.ts
@@ -97,6 +97,14 @@ describe('a module route, with a brand published', () => {
9797 expect(plans).toContain('€19.99 a month')
9898 expect(await render('calendar')).not.toContain('coach-offers')
9999 })
100
101 it('frames the panel on every published module', async () => {
102 for (const module of coachingModules) {
103 const html = await render(module.slug)
104 expect(html).toContain(`<hq-coaching brand="last-soul-ultra"`)
105 expect(html).not.toContain('Not open yet')
106 }
107 })
100108})
101109
102110describe('a module route, with nothing published', () => {
@@ -120,6 +128,27 @@ describe('a module route, with nothing published', () => {
120128 const calendar = await render('calendar')
121129
122130 expect(calendar).toContain('<title>Your week - Last Soul Ultra coaching</title>')
123 expect(calendar).toContain('coach-panel')
131 expect(calendar).toContain('Not open yet')
132 })
133
134 it('frames no panel, and loads no remote script, for a module nobody published', async () => {
135 const calendar = await render('calendar')
136
137 expect(calendar).not.toContain('<hq-coaching')
138 expect(calendar).not.toContain('hq-coaching.js')
139 })
140
141 it('drops the notice that promised an account would open the panel', async () => {
142 // With nothing to sign in to, "needs a training account" is an excuse,
143 // not an explanation.
144 expect(await render('calendar')).not.toContain('Needs a training account')
145 expect(await render('health')).not.toContain('Only you see this')
146 })
147
148 it('leaves the storefront one empty state, not two', async () => {
149 const plans = await render('plans')
150
151 expect([...plans.matchAll(/class="coach-empty"/g)]).toHaveLength(1)
152 expect(plans).toContain('Nothing is open yet')
124153 })
125154})