Commit Graph

350 Commits

Author SHA1 Message Date
martbost 946a9d75b5 Gate consistency video to members; embed on rhythm page; add vertical cut
- Members-only: the stay-consistent video now requires a wallet-verified session
  (server 403 + training-page lock UI), matching Module 3.
- Embedded on /weekly-rhythm above the sheet (hidden in print) with a members-only
  fallback card for signed-out visitors.
- Added a ~31s vertical (1080x1920) social cut, ungated, for Telegram/social.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 06:08:46 -05:00
martbost 47955c6b56 Add "Consistency Wins the Race" training video
A narrated, animated companion to Circle Method Lesson 10: inspires members to
keep the weekly 20-minute rhythm, print and fill the Rhythm sheet each week, and
duplicate-and-teach rather than reinvent. Placed after Lesson 10 in Module 3,
with a watch link added to the printable /weekly-rhythm page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 05:46:58 -05:00
martbost e3f6b00f95 Make event pop-up admin-toggleable; drop it from join pages
The Weekly Huddle overlay now reads its content from /api/announce (config-
driven) instead of a baked-in block, and Admin -> Settings -> Event pop-up
exposes a toggle plus editable fields (eyebrow, flyer, join URL, date, times,
auto-hide, event id). Defaults are seeded into config on boot so the panel
reflects real state. Removed the include from join.html so it never interrupts
the join flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-08 05:28:03 -05:00
martbost be7cb93bc5 Add dismissible Weekly Huddle pop-up overlay (Sept 8)
Self-contained /announce.js injects a branded, dismissible event overlay
(the flyer + all four time zones + Join Google Meet button). Included on the
main funnel and member pages. Auto-expires after the huddle and shows once
per browser until dismissed; edit the EV block to reuse for future events.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-07 15:42:56 -05:00
martbost 44651a92b7 Textbook Play copy: Marty is a top team builder, not a program founder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 15:11:33 -05:00
martbost 106f0fa7bd Training: 'The Textbook Play' coaching video — a real position run by the book
111s in Marty's voice: position #21's ledger, catch-funded climbs, the
pass-over cost, this week's Apex catch-and-climb, and the dashboard/sponsor
next-play close. Chatbot + AI prompt synced.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 14:54:06 -05:00
martbost dd6730e82f Training page: replay callout upgraded to a full attention banner
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 07:04:49 -05:00
martbost 2cf11441ff Replays copy: the team meets at least weekly, sometimes twice
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 06:58:42 -05:00
martbost 79470e471c Members area: weekly webinar replay page at /replays
Wallet-gated like the Method lessons: the API returns titles/dates to
everyone (prospects see the shape of the product) and video URLs only with a
member session. List lives in the data volume (replays.json) so the weekly
add never needs a deploy; videos stream from the DO Spaces bucket (CSP
media-src added). Teaser on /training; chatbot + AI prompt updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 06:46:06 -05:00
martbost 726bc24e09 Traffic Desk UI: trust the bridge's served figure — remove the count-up banner branch here too
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 06:22:16 -05:00
martbost 8ea371dd09 Telegram team posts: disable link previews on payout/team-build notifications
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 05:12:40 -05:00
martbost bf5e8bdee9 Traffic Desk: NAS remaining counts down for banners too — trust the bridge's served field
The count-up theory made every fresh banner read as fully delivered, so the
sweeper deactivated brand-new campaigns minutes after launch and stamped them
complete (caught by Marty 2026-09-01 on ad 2713).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-02 04:59:47 -05:00
martbost 747977b368 Catch eligibility: catcher needs level >= the level the buyer is LEAVING, not the level being bought
Matches _payUpline (level > levelIndex, levelIndex = buyer.level-1). The old
depth+1 rule told #21 (Culmen) it could not catch #49's Apex buy; #21 is #49's
4th matrix upline and does catch it. Fixed in the owner alert, coaching scan,
dashboard pipeline, AI prompt, leader digest, chatbot and how-pay-works copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 10:51:30 -05:00
martbost dcc8202efb Angle squeeze pages: fit headline, video and CTA above the fold
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 09:40:07 -05:00
martbost 71ba633ac5 Angle squeeze pages: quieter header, legal-only footer, hold proof toasts until the hook video ends
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 09:26:25 -05:00
martbost 7bd72e7de7 Clean poster frame for the AdRevSplit explainer
Two problems with pulling the poster straight out of the recording: it caught
the walkthrough's synthetic cursor, and the hero QR was in shot. That QR
resolves to the CURRENT TEAM SPONSOR via the moving link, so anyone scanning
it off the builder entry would route around the AdRevSplit sponsor whose
{affiliateid} the entry is personalised with. Shot fresh with .qr-cta hidden.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:38:01 -05:00
martbost 26d0ff78ad Poster for the RM Circle explainer used in the AdRevSplit downline builder entry
The 88s walkthrough itself lives in the data volume at /tv/rmcircle-what-it-is.mp4
(same route as the Suite tool videos) so it doesn't ride along in the image.
Only the poster frame needs to be static.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 07:33:37 -05:00
martbost 5978ef139f Suite: force the wallet account picker when switching position
Clearing our session was only half the problem. eth_requestAccounts returns
whichever account the wallet already has connected to the site - it never
opens the picker - so a member holding two positions could sign out, switch
accounts in MetaMask, reconnect, and be handed the same address again. Signing
with it resolved to the same position, which looked exactly like the switch
doing nothing.

This is the trap already documented in our own Triple Play guidance, and the
Triple Play is what puts members in this situation in the first place.

Switching now calls wallet_requestPermissions first, which forces the account
selector open. Only on switch, so ordinary single-position sign-ins are
unchanged. And if the wallet still returns the same address, the page now says
so - naming the address and pointing at the wallet's connected-sites list -
instead of silently redisplaying the same position.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:21:44 -05:00
martbost 2d1ccadfd9 Suite: let members switch between positions they hold
A member holding #2 and #3 connected #3's wallet, closed the browser, came
back, and still saw #2. Not a wallet problem: the sign-in cookie lasts 30 days
and there was no sign-out anywhere on the site, so the first position he
authenticated as was pinned to that browser and the wallet was never consulted
again.

This hits precisely the people we tell to buy several positions - the Triple
Play is on /how-pay-works and in the chatbot - so it will keep happening.

Adds POST /api/public/signout (drops the server session and expires the
cookie) and a "not this position? switch" link beside the identity line on
/suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 15:07:29 -05:00
martbost c039c04ceb Suite: entitle the positions above our team root, not just below it
The org test only ever looked downward: you are in if a root appears in your
upline chain. The founders and contract owners sit ABOVE #21, so #2 - a Corona
position that has been here since the beginning - was told "this position isn't
inside our team's organization" and shown a locked wall.

That is backwards. Their positions are the reason the contract exists.

Each root's own upline is now entitled as well, which covers the chain up
through #2 and the root account. A position's upline is fixed at registration,
so it is resolved once and memoised rather than walked per request.

Nothing else changes: level still comes from the contract, the allowlist and
level-override behave as before, and members below the root are unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 14:51:16 -05:00
martbost 7e390aa48e Join: let chosen positions default to direct placement
The moving link is team-first by design: it routes a new join to whoever needs
directs next, so the position whose link was actually clicked earns no entry
reward. Measured on #21 that came to 80 joins in seven days and zero entry
rewards, while the traffic paying for those joins was the owner's.

For a position being deliberately built out, that default is backwards. Any ID
listed in the new directDefaultIds config now behaves as ?direct=1 on its own
/join/<id> link - including every promo tool, flyer QR and downline-builder
entry that resolves through it - without needing the parameter appended.

?direct=0 still forces rotation for a single share, so the team-first behaviour
stays available rather than being replaced.

Config rather than hard-coded, and editable from the admin settings form, so
which positions get this can change without a deploy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 10:29:55 -05:00
martbost 34480d444e Dashboard: let members see what a nudge actually says before and after sending
A member sent a nudge to #102, then went looking for what had gone out. The
only feedback was a small "Delivered to their dashboard" note, so she checked
her clipboard - which still held the unrelated "Copy plain-English summary"
text describing her OWN position. She reasonably concluded the nudge had sent
her position instead of the recipient's.

It had not: the message log shows #34 -> #102 carrying #102's own numbers.
The feature was right and the feedback was wrong, which is its own kind of
bug - a member who cannot verify what they sent will assume the worst.

Every nudge now has a Preview button that names the recipient and shows the
exact text before sending, and on success the panel shows what was sent and
to whom rather than a bare "Delivered".

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 09:42:59 -05:00
martbost 64527af2ab Publish the stagnation video
"The money came. Where did it go?" - 4:06, Marty's voice, rendered from
specs/climb-with-your-catches.json.

Covers why 95% of qualified members cannot cover their next rung despite
having earned past it: the doubling maths, pass-overs being silent, and the
contrast with members who joined days ago and are already climbing. Ends on
the self-paced / afford-to-lose line and a teach-your-two close. No income
claims, no member identified.

Shareable at /v/rmc-climb-catches-d15c3f03fa.mp4 with a poster frame
alongside it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 07:08:37 -05:00
martbost c9aa61e593 Telegram: say what qualifying actually opened, at the moment it happens
Nothing in the system marks the moment a member crosses two directs. That is
the exact point where the job changes from recruiting to climbing, and it is
where the org stalls — 53 qualified members parked at Ascensus, most of them
having treated two directs as the finish line.

The registered-event DM already goes to the referrer. When that registration
is the one that took them to exactly two, it now also tells them they are
qualified, what their next rung costs, which generation it reaches, how many
people they already have sitting there, and that the first catch is double
the rung's price.

It also names the actual failure mode rather than just linking a lesson: the
trap is spending catches instead of climbing with them. The wallet scan says
that is what is happening — 83 of 87 qualified members cannot cover their
next rung despite having earned well past it.

Wrapped so the join notice still sends if the lookup fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 05:22:28 -05:00
martbost 25eddf19c4 Dashboard: show what an upgrade buys, not just what it costs
The upgrade panel showed a price, a funded badge and a button. Nothing on it
said what the member would get. 53 of 87 qualified members in the org are
parked at Ascensus, and a panel that reads as a bill is part of why.

It now leads with what the rung unlocks, in the member's own numbers: the
generation it extends reach to, how many people they already have sitting
there, and what each of those pays when they climb. genCounts already existed
in getOrgShare; this just points it at the member instead of at the admin.

The line that matters most is arithmetic, not persuasion: because every rung
costs exactly twice the last, the first catch at the new level is exactly
double what the upgrade cost. That is Lesson 8's whole argument, and the
panel now makes it where the decision is actually taken rather than leaving
it in a lesson nobody opens at the right moment.

Wrapped in try/catch — if the org walk fails the panel renders as before.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-31 05:18:10 -05:00
martbost c86a583488 Chatbot: the Suite is live, stop telling the bot to hide it
The AI system prompt still carried "CURRENTLY IN PRIVATE TEAM BETA: do NOT
proactively promote it; if a member asks, say it is being finalized and will
be announced soon." That stopped being true when the Suite launched to the
org. /suite has been serving publicly, and the best-performing massifly story
to date sells precisely this — so the bot was deflecting questions about the
strongest answer we have to "what's the actual product".

The prompt now says it is live, open to every position, and should lead any
what-do-I-actually-get question.

chat.js had no Suite entry at all, so the canned side never mentioned it
either. Added one covering the product question in the words people actually
use, with the level ladder and the honest note that in-development tiles are
labelled as such.

Both sides updated together — they drift apart the moment only one is
touched, which is how this one survived the launch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 14:14:02 -05:00
martbost 3f48bdfd99 Traffic Desk: tag ad destinations so clicks are attributable
The Traffic Desk has been unmeasurable since it shipped. track.js reads ?v=
before it reads document.referrer, so every angle-linked ad click was filed
under "angle:graveyard" (or two/phone/pocket) and the referring domain was
discarded. Ad clicks, flyer QR scans, massifly angle links and Telegram angle
links all collapsed into the same four buckets — about 11,400 landings with
no attributable source between them — while networkadspace.com showed one
single visit against roughly 45,000 served impressions.

That reads as "the ads produce nothing." What it actually means is that
nothing carried a source.

Destinations now carry src=nas-banner or src=nas-text. `src` is checked
before `v` in track.js so it wins cleanly, and the landing page reads ?v=
separately so the angle hook is untouched. Split by format because we have
believed text outperforms banners for weeks without being able to show it.

This is the prerequisite for everything else here: pooled Ad Intel
benchmarks, judging which placements are worth expanding, and telling a
member what their impressions bought.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 06:37:41 -05:00
martbost 439c5885de Admin: give the wide tables the full width
The Sponsor Queue table carries min-width:850px but the 1.6fr side of the
split only handed it about 800px, so the Actions column was clipped — the
Reset button cut in half and the delete button off-screen entirely.

Sponsor Queue and Member ID Submissions now run the full width. Add Sponsor
moves below the queue as a single collapsible row of fields instead of a
narrow two-column form in a sidebar, which is both less tall and no longer
squeezing the table next to it.

The rule this follows: a card holding a .table gets the whole width; only
compact forms share a row. Settings keeps its split — those are all short
forms.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 06:23:50 -05:00
martbost 7452490a78 Admin: group the fourteen panels into tabs
The page was a fixed 1.5fr/.75fr split — ten tall panels on the left, four
short config forms on the right. The right column ran out about a quarter of
the way down, so the rest of the page scrolled past an empty gutter, and
finding anything meant scrolling through everything.

Five tabs now, each using the full width: Team (queue, add sponsor, coaching
radar, award capacity, messages), Traffic (conversions, org vs network),
Members (submissions, lookup, matrix), My Money (positions income), Settings
(AI chat, SendGrid, public page).

The panels themselves are untouched — same markup, same ids, same JS. Only
their grouping changed. Panes are moved in the HTML rather than by script, so
the layout does not depend on JS running; the tab file only toggles which
pane is visible and remembers the last one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 06:16:05 -05:00
martbost ee87408dec Admin: move the bonus panel inside the admin view and collapse it
It was sitting between the login panel and #adminView — outside the admin
view entirely, so it rendered above the page header and showed on the login
screen to anyone who loaded /admin.

Now it sits after the Coaching Radar, which is the panel that tells you who
needs a nudge; this is the lever for nudging them. Collapsed by default and
the log is capped at a scrollable 240px, because awarding is occasional and
the log grows every month.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 06:11:41 -05:00
martbost 77dcda6a1a Admin: move the bonus panel script out of the page (CSP)
The site sends script-src 'self'. The panel's logic was an inline <script>
block, so the browser refused to run it: window.agBoot was never defined,
admin.js's guarded call did nothing, and the "What" select stayed empty with
no console-visible cause beyond the CSP report.

admin.html had no inline scripts before this panel — the extraction restores
that, so the next person adding a panel copies a pattern that works.

Fourth thing this CSP has silently broken. Inline scripts are not an option
on this site; new panels ship as files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 06:00:37 -05:00
martbost e4b7d64605 Admin: load the bonus panel after login, not before
The panel fetched its tool list on DOMContentLoaded — which fires while the
admin page is still showing the login form. The request 401'd, the handler
returned silently, and the "What" select was left permanently empty with no
path back.

Loads from the same post-login block as the coaching radar and messages
panels now, plus a refetch if the select is opened while still empty.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:51:26 -05:00
martbost 05afe5c5f6 Admin: award Suite capacity to any member as a team bonus
Marty asked for a way to grant impressions to anybody, labelled as an admin
bonus. Team Grants already did this for leaders, but only within their own
organisation and only out of a level pool.

adminGrant() is deliberately a different thing: it comes from no pool (the
team is not a position and has no allowance), it can reach ANY registered
position rather than only someone downline, and it carries a reason that
survives in the ledger. Recorded with by:0 so nothing downstream mistakes it
for a leader's grant — the member's own page renders those as "the team"
rather than "#0".

The route verifies the position exists on-chain before crediting it; a typo
would otherwise sit in the ledger crediting nobody, and it would be invisible
until someone went looking.

Awards land on top of whatever the level already allows and expire with the
month like every other allowance. The admin page lists what has been awarded
this month, so bonuses stay auditable rather than vanishing once given.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:40:00 -05:00
martbost 59bd4bd4ab Show recorded figures for closed campaigns, not the wiped counter
My own sweep created this. Deactivating an ad ZEROES `remaining` on the
network, and for banners — where that column counts UP — zero reads as
"served nothing". So every campaign the sweep closed flipped to 0 served /
2,500 left, despite the ledger correctly recording served=2500,
completed=true.

Closed campaigns now read from what we recorded at the time rather than
re-deriving from a counter that no longer holds anything. Live campaigns
still compute from the network, per placement type.

Also removed the em-dash placeholder that blanked LEFT for stopped rows —
there is a real number to show — and made stop()'s error path assume fully
served rather than fully unserved, since guessing high would refund
impressions that had actually been delivered.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:33:55 -05:00
martbost 26f2c91684 Banners must stop when they have delivered what was bought
Marty: "if it's just gonna keep running, why would anyone use impressions to
place the ad at all?" He is right, and my previous answer — calling the
over-delivery a bonus — papered over a real flaw.

Banner placements on this network have no cap. Left alone, 2,500 impressions
buys an ad that runs until expiry, which makes the allowance decorative and
the whole level ladder (2,500 at Scintilla to 150,000 at Corona) worth
nothing. Nobody would ever spend the larger allowance for an identical
outcome.

Two changes so the impressions are genuinely the thing being spent:

sweepCompleted() deactivates any campaign that has served its purchased
amount. Runs 90s after boot and every 15 minutes, batches its stat reads, and
only touches campaigns that are live, un-stopped and provably at or past what
was bought. Nothing is refunded — they delivered in full. It re-reads the
ledger after the network calls, since deactivating takes real time and
another member may have launched in that window.

New campaigns get days:30 instead of days:365. The allowance is monthly, so a
campaign outliving the month it was paid from is the same bug by another
route. Belt and braces — whichever ends it first.

Copy corrected too: "still serving as a bonus" became "served in full —
closing out", and the footnote now states plainly that a campaign ends when
it has served what was bought.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:23:38 -05:00
martbost 1b5ae64905 Traffic Desk: say "delivered" when a banner has served what was bought
Marty asked why banners still show "running" with 0 left. They genuinely are
running — banners on this network have no cap. The counter runs past the
purchase and the ad keeps rotating until its 365-day expiry, so a
fully-delivered banner is still serving. #2707 is at ~6,129 on a 2,500 buy.

"running / LEFT 0" reads as broken though, so the row now says "delivered —
still serving as a bonus, Stop to end it", and the footnote explains it once:
the extra costs nothing further against their allowance, and Stop actually
ends it on the network.

Worth flagging what this does NOT do: nothing auto-stops a banner at its
purchased amount. Members get free over-delivery, which is fine on our own
inventory, but it means every banner ever launched keeps rotating for a year
unless someone stops it. If that becomes noisy, the fix is a scheduled job
that deactivates ads once served >= bought — deliberately not built now,
since it needs a cron and the over-delivery currently costs nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 05:18:31 -05:00
martbost 37c9fc615c Traffic Desk: the network counts banners and text ads in OPPOSITE directions
Marty reported banner impressions never moving while text ads did. They were
serving the whole time — we were reading the counter backwards.

Measured against the live network, because none of this is documented:
  TEXT   `remaining` counts DOWN from the purchase to zero.
         ad 2689: 1,111 left mid-flight, 0 once complete.
  BANNER `remaining` counts UP as impressions deliver, straight past the
         amount bought. ad 2707: 3,521 one day, 6,129 the next, on a 2,500
         buy. Every live banner sits above its purchase and rising.

Reading both as count-down made every banner report "0 served" while quietly
delivering thousands — and made stop() treat delivered impressions as
unserved, so stopping a finished banner refunded the lot. That was giving
back inventory that had already been spent on the network.

interpret(kind, bought, stat) now resolves both directions in one place, used
by stop() and by the table. served + left always reconciles to what was
bought and neither can exceed it.

This also supersedes the earlier "counter drift" note: the drift WAS the
count-up, seen through a count-down lens.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 04:56:51 -05:00
martbost 86bba1b605 Actually fix the missing joiner alert — the org test, not the emit
My previous fix was at the wrong layer. Proof: the announced-once set shows a
"reg:" key written at 13:53:40, so the registered event fired correctly. It
was being discarded one layer later.

The real cause: the team-topic filter asks isInTeam(newMemberId), which walks
uplineId — and a brand-new member has no uplineId at the moment the event
fires. The indexer wires the matrix in a SEPARATE pass after the log loop
(chain.js:359), so at emit time the walk has nothing to follow, the org test
returns false, and the announcement is dropped. Payouts survive because they
reference existing members whose uplines are already known. That asymmetry is
exactly what Marty kept seeing: the member who got paid, never the joiner.

Registered events now test the referrer as well. The referrer is always
already wired, and anyone joining under someone in the org is in the org, so
it is correct as well as available. Confirmed on #255: uplineId 246,
referrerId 246 — the referrer was resolvable the whole time.

Keeping the announced-once change from before: it fixes a real second bug
(snapshot landing first suppressed the emit) and both had to be right.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 09:03:18 -05:00
martbost 4982897613 Fix missing "who joined / what they unlocked" alongside payout alerts
Marty saw a payout land in the team topic with no registration event beside
it. Confirmed on-chain: #254 registered at 12:49 and the referral payout to
#237 fired at 12:49 — the payout announced, the registration did not. #254 is
inside the org, so the team filter was not the cause.

The bug: registered/upgraded were emitted only when the member was NEW to our
state, or when their stored level was lower than the log said. That conflates
"is this member new to us?" with "have we announced this event?". A snapshot
reads member storage straight from the contract, so if one lands between
someone registering and us scanning that block, the member is already on file
and the announcement is silently skipped — while the payout it triggered goes
out regardless. Exactly the asymmetry reported.

Now keyed on the on-chain event itself (tx + type + id), so each announces
exactly once no matter which code path notices it first. The scan window
still bounds it — we only look past lastBlock — so it cannot replay history.
The set self-trims at 4000 keys.

Also fixed while here: the upgraded event never set `level`, only `newLevel`,
while every consumer reads evt.level. The unlock text only worked because it
could fall back to resolving the level by NAME.

Payout announcements are deliberately untouched: their dedupe depends on
snapshot rows having no tx, so keying them this way risks announcing historic
payouts to everyone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 07:57:11 -05:00
martbost caeaf56127 Suite overview video at the top of the tool wall
A 2m35 tour of all 19 tools at 50,000 feet — what each one is, tier by tier,
without opening any of them. It scrolls the wall as it names each level, so
the viewer sees the thing being described.

Recorded the same way as the per-tool walkthroughs: narration generated first
in Marty's cloned voice, then the browser driven on the measured clip
timings, so speech and scrolling cannot drift apart.

Closes with the line that matters most: these tools make the work lighter,
they do not do the work — getting your two and helping them get theirs is
still the job. Same reason the individual walkthroughs carry their caveats
out loud rather than only on the page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 07:25:48 -05:00
martbost 962f0c8892 Stop offering "Connect Telegram" to members who are already connected
Janie linked successfully and the dashboard kept showing the Connect button,
so a working link looked like a failed one — and she had no way to tell which.

The Messages panel had no idea about link state: msg-inbox never reported it.
It now returns tgLinked (from tgbot.memberChat) on the response the panel
already fetches, so this costs no extra request. Linked members get a
confirmation telling them where their pings arrive and how to open the Mini
App, instead of a button asking them to do something already done.

Third variant today of the same failure: the interface not knowing a state it
could easily have known, and the member left guessing. The bot had it too —
telling an already-linked member their code had expired.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 07:05:26 -05:00
martbost 8e7660a9a1 Load nav-dash on the two pages that were missing it
Printable Handouts and Generation Pay both have a .nav-actions bar but never
loaded nav-dash.js, so they got neither the dashboard shortcut nor the Suite
link. Every other member page already includes it.

Also worth recording: the audit's 'no nav on Fast Start / Weekly Rhythm' flag
was the audit's own fault — those are print sheets carrying .scr-nav rather
than .nav-actions, and the screen-only bar shipped correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:44:08 -05:00
martbost 8262b382e6 Navigation audit fixes: duplicates, mobile invisibility, and two dead ends
Ran a signed-in walkthrough of 23 pages at desktop and mobile widths. Three
real problems.

1. ELEVEN Suite pages showed "My Dashboard" TWICE — they ship their own
   button and nav-dash.js injected a second one. It now bails if a /my link
   is already present.

2. The Suite link was hide-mobile, so on a phone — where most members are —
   the toolkit was invisible on every page except the dashboard. That is
   precisely backwards for the tool we just launched to the whole org. It is
   now visible at every width.

3. Fast Start and Weekly Rhythm had NO navigation at all and an unclickable
   brand mark: a member landing there was stuck with the browser back button.
   They are PRINT sheets, so bolting the site header on would have wrecked
   what they are for — they now carry a small screen-only bar that is hidden
   by @media print.

Printable Handouts and Generation Pay were also missing the Suite link; they
pick it up from nav-dash now that it is no longer hidden on mobile.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:39:42 -05:00
martbost b5cb63f15c Add a Suite link to the member nav — it was unreachable
The Circle Suite went live to all 216 positions, but nothing outside its own
pages linked to it. Members could only reach it by knowing the URL, which
means most of them could not reach it at all.

The dashboard now leads with it: "My Tools" is the primary action, and Promo
Tools drops to secondary — the Promo Center is itself a level-1 Suite tool, so
the Suite is its parent rather than its sibling.

Everywhere else it rides the existing nav-dash.js injection, which already
runs on every member page and already knows who the visitor is. Same identity
test as the dashboard button: if we know who they are, they hold a position,
and a position is a licence to the toolkit. Prospects still see nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:33:30 -05:00
martbost bd42708fa4 Telegram bot: don't tell a linked member their code expired
Janie tapped a stale link code, got "That link code is expired or already
used", and immediately below it "Linked to position #34!". Both messages were
accurate — two different codes, the old one tapped first — but back to back
they read as a contradiction, and the expired message sends someone back to
the dashboard to redo something already done.

The bot now checks whether that chat is already linked before reporting a
dead code, and says so plainly: already linked to #34, nothing more to do,
here is what you can type. Only genuinely unlinked members get the
get-a-fresh-code path, and that copy now explains WHY codes die (single-use,
short-lived) and says to use the newest link.

Verified against live data: chat 652806260 -> position #34, so the link was
real and only the message was wrong.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:29:23 -05:00
martbost 9f3f28c4bf Connect Telegram: stop relying on window.open inside wallet browsers
Janie can now sign in from Trust's DApp browser but cannot link Telegram.
Cause: the handler called window.open(url,'_blank'), and wallet DApp browsers
routinely block programmatic window.open. The call does nothing, no error is
raised, and the member is left staring at an unchanged screen.

Now the button fetches the link and then renders a real anchor for the member
to tap — an actual tap is handled by the OS, which is what launches the
Telegram app — plus a "copy the link" button with an execCommand fallback for
webviews that refuse both.

Third failure today from the same root assumption: that members are in a
normal browser. They are usually in someone's webview.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:22:43 -05:00
martbost 8964dfcf97 Detect in-app browsers — the actual reason #34 could not connect
Janie's screenshot showed the X / chevron / share / kebab chrome of TELEGRAM'S
in-app browser, not Trust's DApp browser. She tapped her sponsor's link in a
Telegram chat, Telegram opened it in its own webview, and that webview can
never hold a wallet. The page said "no wallet in this browser", she said she
was using Trust — both true, completely at cross purposes.

Our advice made it worse: "open your wallet's browser and type the address" is
useless to someone whose whole reason for being on the page is that they
tapped a link.

Now we name the browser they are actually in — Telegram, Facebook, Instagram,
LINE, Messenger, X, or a generic Android WebView — say plainly that it cannot
hold a wallet and that this is not their fault, and give them the one useful
thing: a button that copies the address to the clipboard so they can paste it
into Trust. There is an execCommand fallback because older webviews have no
clipboard API, and leaving a button that silently does nothing would be worse
than not having one.

Detection deliberately requires the absence of window.ethereum before calling
something an in-app browser: wallet browsers ARE WebViews, and those are
exactly the ones that work. Verified against Telegram, Android WebView, Trust
(correctly ignored) and mobile Safari (correctly ignored).

This will hit far more members than Janie — every link tapped inside a chat
app lands in the same trap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:18:35 -05:00
martbost a57af32f4a Co-Branded Pages: give Fastigium a change of kind, not a bigger number
Once every member from Ascensus up could hold several pages, the Funnel
Factory's differentiator became a count — the same quantity-not-kind flaw the
upper tiers had. L6 now unlocks identity instead: a member's own name,
monogram and accent colour on every page they build. Below Fastigium you have
a page on the team site; at Fastigium it becomes yours.

Three deliberate limits.

NO IMAGE UPLOADS. A logo file uploaded by a member and served from
rmcircle.team is an abuse surface — whatever they upload lives on our domain.
The mark is a monogram generated from their brand name instead: real identity,
nothing to moderate. Same reasoning that keeps Traffic Desk creative
team-only.

ACCENTS ARE A CURATED SET, not a colour picker. Every option is checked
against the dark ground, so nobody can pick something that makes their own
page unreadable and then wonder why it converts badly.

THE DISCLAIMERS ARE NOT THEIRS TO BRAND. Branding replaces the header
identity only; the footer — independent team resource, no income guaranteed,
cryptocurrency risk — is rendered by us and cannot be removed or restyled.
Co-branding, not white-labelling. Verified in the renderer.

Branding is resolved against the LIVE contract level at render time, so a
position that drops below Fastigium reverts to team identity on its own
rather than keeping something it no longer holds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:14:28 -05:00
martbost c8ae4d55b0 Page Builder: members can hold several pages, scaled by level
The quota and the capability used to contradict each other. An Ascensus
member was allowed 3 page builds a month, but every build overwrote the same
file — so they could build three times and never have more than one page. The
slug plumbing to do better already existed and was only exposed at level 6,
which made the Funnel Factory's differentiator a COUNT rather than a KIND, the
same flaw the upper tiers had.

Now: hold caps by level (2 at Ascensus rising to 50 at Corona), separate from
the monthly build quota. Rebuilding spends a build but needs no hold room;
adding a new page needs both. The Page Builder is a list of your pages with
view / copy link / rebuild / delete, plus a form that ADDS one instead of
silently replacing what you had.

Two things kept deliberately fixed. The first page a member builds stays at
/p/<id> forever and cannot be deleted — that address ends up on banners,
printed flyers and in people's DMs, so it must never move. Extra pages take a
slug from the headline the writer produced, de-duplicated, so the address
describes the page without anyone having to invent one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 06:02:06 -05:00
martbost 3620a72338 Wallet diagnostics: a check page, and an error that names the signer
Two sign-in failures with different causes and no way to tell them apart from
the outside, so stop guessing and get the data off the member's device.

/wallet-check reports what the page can actually see: whether window.ethereum
exists, which wallet flags are set, which EIP-6963 wallets announced, the
accounts and chain the wallet returns — and critically, any CSP violation.
Our script-src is 'self' with no 'unsafe-inline', and a wallet browser that
injects its provider via a script tag would be blocked silently, producing
symptoms identical to "no wallet installed". The company dApp sends no CSP at
all, which is a plausible reason it connects where we do not. This page will
confirm or kill that theory rather than us theorising further.

"Signature does not match this wallet" was true but useless — it never said
WHICH wallet signed. It now names both the address the page asked for and the
address that actually signed, which identifies the classic multi-account case
(wallet signs with the selected account, not the one the page picked up) in
one glance.

Verified for the case in hand: 0x20E0…2248 IS the wallet registered to
position #52 on-chain, so ownership was never the problem.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-29 05:53:34 -05:00