Skip to content

One clock, one DAILY panel, and a prune that knows what /stats reads - #215

Merged
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3
Jul 31, 2026
Merged

One clock, one DAILY panel, and a prune that knows what /stats reads#215
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3

Conversation

@0xward

@0xward 0xward commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Closes the two items the audit (§9b) deliberately left open, plus the thing the owner spotted while watching the map.

No smart contract, ABI, or contract address is touched.


1. The DAILY menu opened somewhere else

"itu sebuah menu yang kalo di klik yang muncul malah di luar, bukan di dalam menu daily itu sendiri"

Exactly right, and it was worse than misplaced:

  • Every other rail button — Rewards, Pass, Bag, Shop, Craft, Settings — opens a screen. DAILY dispatched an event that expanded a strip at the bottom of the map, nowhere near the icon pressed.
  • The strip held only the contracts, while the streak, fragments, energy and craft — all equally daily — sat as chips somewhere else.

"Daily" was three places, and one of them behaved unlike its neighbours.

It is one panel now, holding all five, opened by the rail button or by any chip. The chips stay on the bar because they are the glance and what is at risk has to be visible without a tap; the panel is the detail, and there is one of it. Closes on ✕ or on tapping outside — a panel over a map that can only be dismissed one way is a trap.

2. Two clocks on one bar

Energy refilled on a rolling 24h from first use while contracts and the streak reset at 00:00 UTC. A player who played at 20:00 got new contracts four hours later and new energy twenty-four hours later, with nothing on screen explaining the difference.

Energy is on the UTC day now. windowStart keeps its name and type and holds the day it was spent against, so records written by the old code compare as an earlier day and reset on first read — no migration, and nobody gains or loses a run at the changeover. windowHours is deleted: nothing read it once the boundary moved, and a config value nothing reads is exactly how game-config.ts became a liability.

This is slightly more generous (play at 20:00, allowance back at midnight). A deliberate trade — the $1 refill sells to players who want more than five in one sitting, and that motivation is untouched.

What it buys is one clock, and it is what lets the panel show a single honest countdown. "Resets in 1h 27m" over three chips on two different clocks would have been a lie about one of them.

3. Nothing pruned the daily/weekly buckets

prune() now runs on the same daily cron: dailyContracts, dailyContractClaims, passPerkClaims older than 30 days, and vaultFragments older than 8 weeks.

It does not touch paperClaims, goldenKeyClaims or vaultCompleted/api/stats counts lifetime totals out of those three. Pruning them would shrink the public stats page, and the drop would read as players leaving rather than as rows being deleted. There is a test naming each of the three specifically.

A key whose shape is not recognised is never stale, so a malformed bucket is left alone rather than guessed at. The prune is caught, not thrown: housekeeping failing must never stop a season being frozen.

Verification

Check
npm run test:season 66/66 (was 45 — 21 new, most of them about what the prune must not delete)
npm run test:contracts rewritten for the panel — close button, tap-outside, one countdown, and that DAILY opens something holding more than contracts
test:play · test:raid · test:floors · test:audio · test:streak-ui · test:bunkers-ui · test:season-routes all pass
test:streak · test:seeded · test:bunkers · test:inventory · test:migrate 31/31 · 15/15 · 34/34 · 7/7 · 14/14
test:traversable 0 failures
audit · check:copy · check:market · check:cssvars · check:attribution all pass
tsc --noEmit clean
lint 33 — unchanged
build succeeds

Docs: §5.5 gains the panel and the reason it is a panel, §9b records both closed items, and game-mechanics.md now tells players everything daily resets together at 00:00 UTC.


Generated by Claude Code

Closes the two items §9b left open, and the thing the owner spotted while
watching the map.

1. THE DAILY MENU OPENED SOMEWHERE ELSE

Owner: "itu sebuah menu yang kalo di klik yang muncul malah di luar, bukan
di dalam menu daily itu sendiri."

Exactly right, and it was worse than misplaced. Every other rail button —
Rewards, Pass, Bag, Shop, Craft, Settings — opens a screen. DAILY dispatched
an event that expanded a strip at the BOTTOM of the map, nowhere near the
icon pressed. And the strip held only the contracts, while the streak,
fragments, energy and craft — all equally daily — sat as chips somewhere
else. "Daily" was three places, and one of them behaved unlike its
neighbours.

It is one panel now, holding all five, opened by the rail button OR by any
chip. The chips stay on the bar because they are the glance and what is at
risk has to be visible without a tap; the panel is the detail, and there is
one of it. Closes on ✕ or on tapping outside — a panel over a map that can
only be dismissed one way is a trap.

2. TWO CLOCKS ON ONE BAR

Energy refilled on a rolling 24h from first use while contracts and the
streak reset at 00:00 UTC. A player who played at 20:00 got new contracts
four hours later and new energy twenty-four hours later, with nothing on
screen explaining the difference. Energy is on the UTC day now.

windowStart keeps its name and type and holds the day it was spent against,
so records written by the old code compare as an earlier day and reset on
first read: no migration, and nobody gains or loses a run at the changeover.
windowHours is deleted — nothing read it once the boundary moved, and a
config value nothing reads is exactly how game-config.ts became a liability.

Slightly more generous (play at 20:00, allowance back at midnight). A
deliberate trade: the $1 refill sells to players who want more than five in
one sitting, and that motivation is untouched. What it buys is one clock —
and it is what lets the panel show a single honest countdown. A "resets in
1h 27m" over three chips on two different clocks would have been a lie about
one of them.

3. NOTHING PRUNED THE DAILY/WEEKLY BUCKETS

prune() now runs on the same daily cron: dailyContracts,
dailyContractClaims and passPerkClaims older than 30 days, vaultFragments
older than 8 weeks.

It does NOT touch paperClaims, goldenKeyClaims or vaultCompleted, because
/api/stats counts LIFETIME totals out of those three — pruning them would
shrink the public stats page, and the drop would read as players leaving
rather than as rows being deleted. There is a test naming each of the three.
A key whose shape is not recognised is never stale, so a malformed bucket is
left alone rather than guessed at. The prune is caught, not thrown: a
housekeeping failure must never stop a season being frozen.

test:season 45 -> 66. test:contracts rewritten for the panel, including
that DAILY opens something holding more than contracts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A764RdnwpyWnG7uCNhMiQ
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nullstate Ready Ready Preview Jul 31, 2026 11:14am

@0xward
0xward merged commit 84f0e9d into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant