Skip to main content
← Back to list
01Issue
BugShippedSwamp ClubPublic
Assigneeskeeb

Relationships

#1654 Profile XP bar jumps ~25px on fill, and pays the full cumulative walk for two numbers board_totals already holds

Opened by keeb · 8/14/2026· Shipped 8/15/2026

The XP bar's pending state is the worst-looking part of #1503, and the read behind it is also the most over-served. Both are fixable, and probably together.

The visual problem

islands/ProfileXpBar.tsx renders a h-[3px] accent stripe until the fill lands, then swaps in the real bar, whose inner row is height: 28px plus a bottom border (components/XpBar.tsx:39). So every profile above the fold jumps ~25px a few hundred milliseconds after paint, and everything below it moves.

Two things make it worse than a normal skeleton. The 3px stripe is not a placeholder — it is a REAL state, the one founders and max-tier operatives get permanently, so for the first moment the page is actively telling you something false about the operative rather than telling you it is loading. And the swap lands in the dossier panel's top stripe slot, above the identity block, so the whole page shifts rather than one section.

Minimum fix regardless of anything else: reserve the final height in the pending state so the fill cannot move layout. That is a CLS bug on the site's most-linked page type.

The read behind it

Measured on prod, GET /api/v1/users/<name>/score-card TTFB over six samples: 0.50, 1.49, 0.70, 1.03, 0.51, 0.50 — median ~0.6s, p-worst ~1.5s. That is the whole fill, and the XP bar is one of its four consumers.

The endpoint calls loadProfileScore, which runs dailyHistory — the full cumulative walk over the operative's entire history — to produce a headline total, a streak and one row of counters. For the XP bar specifically it needs exactly two numbers: the canonical score, and the tier ordinal derived from it.

swamp.board_totals already holds that, at owner grain, keyed, one row per owner (32,166 rows locally): owner, t_all, t_week, t_today, active_days, day_bound. The score-rollup projector rewrites it in full every run — #1575 built it precisely so the boards could stop recomputing the owner population, and it made board cost a function of the owner population alone. A single keyed lookup there is a different order of cost from the walk.

What has to be decided, not just implemented

Using board_totals for the profile trades against a documented contract. CLAUDE.md, on the substrate split: the profile/keyed and locate/rank/slice reads deliberately KEEP the today tail "so /u/{name} shows an operative's own XP before the board does; that divergence is the contract, not a bug."

So the question is whether that contract is worth ~0.6-1.5s on every profile view. Some observations that bear on it:

  • board_totals carries t_today, so it is not "excludes today" — it lags by one projector cadence, not by a day. The gap is much smaller than the contract's framing implies.
  • The contract exists so an operative sees their own points move promptly after doing work. An XP PROGRESS BAR is the least sensitive consumer of that: it is a fraction toward the next rung, and one cadence of lag is invisible on it. The headline "· N pts" is where the contract earns its keep.
  • So this may not be all-or-nothing. The bar (and the Tier section, which shows the same ladder position) could read the cheap substrate while the headline keeps the walk — or the whole card could move and the contract be retired deliberately.

Options

  1. Reserve the height only. Fixes the jump, changes no reads. Should happen regardless of what else is chosen.
  2. Serve the card from board_totals. Cheapest by far; costs the today-tail contract. Needs the freshness question answered first.
  3. Split the substrates. Ladder position (XP bar, Tier section) from board_totals; headline and streak from the walk. Keeps the contract where it matters, but means two reads and therefore two fills, which fights the one-request rule the fill store exists to enforce.
  4. Cache the endpoint per username. Reuses the board-freshness machinery from #1573 and leaves substrates alone, but profile views are long-tail so it only helps repeat views — it does nothing for a cold profile, which is the case that hurts.

Option 1 plus a decision between 2 and 4 is probably the shape.

Context

Shipped in #1503 (PR #1079); the founder-accent follow-up is PR #1081. The fill is one request shared by four islands through lib/profile-score-fill.ts, so whatever the card reads, it is read once per render — this is about making that one read cheap, not about making it rarer.

02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 11 MOREREVIEW+ 3 MOREPR_MERGED

Shipped

8/15/2026, 12:54:09 AM

No activity in this phase yet.

03Sludge Pulse
keeb assigned keeb8/14/2026, 9:27:31 PM

Sign in to post a ripple.