TimeCurve: unify pre-open countdown to startSaleAt with indexer clock and "TimeCurve Opens In" copy #115

Closed
opened 2026-05-03 10:47:50 +00:00 by PlasticDigits · 7 comments
PlasticDigits commented 2026-05-03 10:47:50 +00:00 (Migrated from gitlab.com)

Summary

Unify all TimeCurve-facing countdown UX so that before onchain sale goes live (block.timestamp < TimeCurve.saleStart() after startSaleAt(epoch) / #114), every prominent timer counts down to saleStart, with explicit copy "TimeCurve Opens In", using the same indexer-anchored chain clock as the hero timer (useTimecurveHeroTimer, /v1/timecurve/chain-timer, #48).

Once the sale is live, timers should continue to count down to deadline() (round timer) with existing phase-specific narratives where appropriate.

Problem statement (current behavior)

  • /timecurve (Simple) already switches the displayed countdown between pre-start and live using preStartCountdownSec vs saleCountdownSec in useTimeCurveSaleSession and TimeCurveSimplePage.
  • useTimecurveHeroTimer always derives secondsRemaining as deadline − chainNow from indexer fields deadline_sec, block_timestamp_sec, … (indexer/src/chain_timer.rs).
  • After startSaleAt, deadline = saleStart + initialTimerSec (TimeCurve.sol). During saleStartPending, deadline − chainNow = (saleStart − chainNow) + initialTimerSec — i.e. not “opens in” — but /timecurve/arena passes this hero secondsRemaining straight into the hero clock (TimeCurveArenaView, useTimeCurveArenaModel), so Arena’s digits disagree with Simple in the pre-open window.
  • Copy is inconsistent: Simple uses section title "Time left" and phase narrative from phaseNarrative, not a universal "TimeCurve Opens In" line.

Inventory — where timers / countdowns appear

Location What it shows today Notes
LaunchGate.tsx + LaunchCountdownPage.tsx Countdown to VITE_LAUNCH_TIMESTAMP (build-time marketing gate) Not startSaleAt. Out of scope unless product wants copy alignment only. Eyebrow today: "DOUB launches in".
TimeCurveSimplePage.tsx + TimeCurveTimerHero.tsx Pre-start: saleStart − ledger; live: deadline − chainNow (via session) Needs "TimeCurve Opens In" (pre-start) + verify indexer clock usage matches spec. Section title "Time left" may need phase-specific label.
TimeCurveArenaView.tsx Hero clock uses raw secondsRemaining from useTimecurveHeroTimer Must branch for saleStartPending like Simple.
TimeCurveSections.tsx (Arena standings / raw accordion) deadline row + secondsRemaining seconds Should show open vs round semantics consistently in pre-start.
TimeCurveLiveCharts.tsx Chart window uses saleStartSec / deadlineSec Confirm no misleading “progress” before open.
TimeCurveProtocolPage.tsx Read-only deadline (and related) Optional: add saleStart row if missing; informational, not a ticking hero.
TimecurveBuyModals.tsx Buy detail new_deadline TX-scoped; unchanged unless copy audit requests.
Cooldown strings (formatCountdown in Simple / Arena) Wallet buy cooldown Not sale open; leave as-is unless confusing next to new label.

Data / information needed

  1. Onchain (source of truth)

    • TimeCurve.saleStart() — epoch set by startSaleAt (#114).
    • TimeCurve.deadline() — round end; moves with buys after open.
    • TimeCurve.ended().
  2. Indexer-anchored clock (UX consistency, #48)

    • Today: block_timestamp_sec + skew in useTimecurveHeroTimer.
    • Optional extension: sale_start_sec on GET /v1/timecurve/chain-timer so the same snapshot carries both targets and avoids RPC/indexer drift for the target timestamps (deadline already comes from indexer poll).
  3. Phase

    • Reuse derivePhase / ledgerSecIntForPhase so no page invents a separate “is it open?” rule.

File: indexer/src/chain_timer.rs

  • Add one saleStart() eth_call in the same block_id batch as deadline, timerCapSec, ended, podium.
  • Extend ChainTimerSnapshot JSON with sale_start_sec (decimal string; "0" when not scheduled — matches contract).
  • Bump API x-schema-version in indexer/src/api.rs per project convention; document in docs/indexer/design.md if applicable.

Frontend: extend TimecurveChainTimer type + snapshotFromIndexerChainTimer to parse sale_start_sec.

Alternative (minimal indexer): keep indexer payload as-is; read saleStart only via wagmi / useReadContracts and combine with chainNowSec from useTimecurveHeroTimer — satisfies [#48] clock alignment but two sources for target times; document tradeoff.

Proposed frontend changes

Core logic (refactor)

  1. Single derived “hero display seconds”

    • Input: phase (or booleans), saleStartSec, deadlineSec, chainNowSec (from hero hook), fallbacks from ledgerSecIntForPhase.
    • Output:
      • saleStartPending → max(0, saleStartSec − floor(chainNow))
      • saleActive / saleExpiredAwaitingEnd → existing max(0, deadline − chainNow) (from indexer deadline snapshot)
      • other phases → undefined or 0 per existing UX

    Candidate homes (pick one in implementation):

    • Extend useTimecurveHeroTimer to accept saleStartSec + phase and return displaySecondsRemaining; or
    • New small helper in timeCurveSimplePhase.ts / formatTimer.ts used by both useTimeCurveSaleSession and useTimeCurveArenaModel.
  2. Replace Arena’s raw secondsRemaining for the big digit display with the derived value above; keep internal uses (e.g. timer extension preview, timerCapSec - secondsRemaining) on deadline − chainNow only when sale is live — pre-start should not feed “live round” math.

  3. Copy

    • Pre-start hero label: "TimeCurve Opens In" (visible title / eyebrow / aria-live string as appropriate).
    • Align phaseNarrative / TimeCurveSubnav hints if they duplicate timer meaning.

Files likely to touch

Indexer (if extended): indexer/src/chain_timer.rs, indexer/src/api.rs, indexer/tests/integration_stage2.rs

Acceptance criteria

  • When derivePhase === saleStartPending, Simple and Arena hero countdown digits both equal max(0, saleStart − indexerAnchoredChainNow) (within 1s tolerance vs cast call on saleStart + head block).
  • Prominent UI copy includes the exact phrase "TimeCurve Opens In" (or equivalent screen-reader text) during saleStartPending on both routes.
  • When the sale becomes live, the same hero components without full page reload switch to counting down to deadline with no stale “opens in” label.
  • No regression to #48: phase strip / buy gating must not disagree with hero digits because of split clocks when indexer snapshot exists.
  • If VITE_INDEXER_URL is unset or chain-timer unreachable, behavior matches existing fallback policy (document any intentional difference).

Verification criteria

  1. Unit tests: add cases for “pre-start display seconds” vs “live seconds” in timeCurveSimplePhase.test.ts or a dedicated helper test file; include edge: saleStartSec == 0 (loading), ledgerSecInt just before/after saleStart.
  2. Local Anvil: use YIELDOMEGA_DEV_SALE_START_DELAY_SEC / stack script behavior from #114 so saleStart is in the future; open /timecurve and /timecurve/arena side-by-side — digits and "TimeCurve Opens In" must match.
  3. Cross-check: cast call (or read contract UI) for saleStart, deadline, latest block.timestamp vs displayed remaining seconds.
  4. Indexer optional path: if sale_start_sec is added, assert JSON field present and matches RPC at same read_block_number.
  5. Regression: post-open, buy once and confirm deadline extension still updates hero; WarBow / cooldown copy unchanged.

Checklists

Implementation

  • Identify every user-visible countdown on TimeCurve routes (table above + grep for formatLaunchCountdown, formatCountdown, secondsRemaining in frontend/src/pages/timecurve/ and timeCurveArena/).
  • Implement shared derived countdown API; remove Arena’s incorrect use of deadline - now during pre-start.
  • Add "TimeCurve Opens In" to Simple + Arena hero surfaces; update aria-live strings.
  • (Optional) Extend indexer chain-timer with sale_start_sec; update indexerApi + hook.
  • Update docs / invariants cross-links.
  • Run npm run typecheck, npm test, targeted e2e if copy/selectors change.

Code review

  • Single phase machine: no new sale not live branching outside derivePhase / ledgerSecIntForPhase.
  • Timer extension / timerCapSec math uses live deadline semantics only.
  • Schema/version bump for indexer if API extended.
  • No drive-by refactors unrelated to timers.

QA (manual)

  • Pre-start: Simple + Arena show same remaining sec to saleStart.
  • At saleStart boundary: both flip to round countdown within ~1s.
  • Indexer offline / backoff (#96): no crash; stale/offline messaging unchanged or improved.
  • prefers-reduced-motion: sparks/animations still respect global rules.
  • Mobile width: digit layout stable (tabular nums).
## Summary Unify **all TimeCurve-facing countdown UX** so that **before** onchain sale goes live (`block.timestamp < TimeCurve.saleStart()` after `startSaleAt(epoch)` / [#114](https://gitlab.com/PlasticDigits/yieldomega/-/issues/114)), every prominent timer counts down to **`saleStart`**, with explicit copy **"TimeCurve Opens In"**, using the **same indexer-anchored chain clock** as the hero timer ([`useTimecurveHeroTimer`](frontend/src/pages/timecurve/useTimecurveHeroTimer.ts), `/v1/timecurve/chain-timer`, [#48](https://gitlab.com/PlasticDigits/yieldomega/-/issues/48)). Once the sale is live, timers should continue to count down to **`deadline()`** (round timer) with existing phase-specific narratives where appropriate. ## Problem statement (current behavior) - **`/timecurve` (Simple)** already switches the displayed countdown between pre-start and live using `preStartCountdownSec` vs `saleCountdownSec` in [`useTimeCurveSaleSession`](frontend/src/pages/timecurve/useTimeCurveSaleSession.ts) and [`TimeCurveSimplePage`](frontend/src/pages/TimeCurveSimplePage.tsx). - **`useTimecurveHeroTimer`** always derives `secondsRemaining` as **`deadline − chainNow`** from indexer fields [`deadline_sec`, `block_timestamp_sec`, …](frontend/src/lib/indexerApi.ts) ([`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs)). - After `startSaleAt`, **`deadline = saleStart + initialTimerSec`** ([`TimeCurve.sol`](contracts/src/TimeCurve.sol)). During **`saleStartPending`**, **`deadline − chainNow` = (saleStart − chainNow) + initialTimerSec** — i.e. **not** “opens in” — but **`/timecurve/arena`** passes this hero `secondsRemaining` straight into the hero clock ([`TimeCurveArenaView`](frontend/src/pages/timeCurveArena/TimeCurveArenaView.tsx), [`useTimeCurveArenaModel`](frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx)), so **Arena’s digits disagree with Simple** in the pre-open window. - Copy is inconsistent: Simple uses section title **"Time left"** and phase narrative from [`phaseNarrative`](frontend/src/pages/timecurve/timeCurveSimplePhase.ts), not a universal **"TimeCurve Opens In"** line. ## Inventory — where timers / countdowns appear | Location | What it shows today | Notes | |----------|---------------------|--------| | [`LaunchGate.tsx`](frontend/src/app/LaunchGate.tsx) + [`LaunchCountdownPage.tsx`](frontend/src/pages/LaunchCountdownPage.tsx) | Countdown to **`VITE_LAUNCH_TIMESTAMP`** (build-time marketing gate) | **Not** `startSaleAt`. Out of scope unless product wants copy alignment only. Eyebrow today: "DOUB launches in". | | [`TimeCurveSimplePage.tsx`](frontend/src/pages/TimeCurveSimplePage.tsx) + [`TimeCurveTimerHero.tsx`](frontend/src/pages/timecurve/TimeCurveTimerHero.tsx) | Pre-start: `saleStart − ledger`; live: `deadline − chainNow` (via session) | Needs **"TimeCurve Opens In"** (pre-start) + verify indexer clock usage matches spec. Section title **"Time left"** may need phase-specific label. | | [`TimeCurveArenaView.tsx`](frontend/src/pages/timeCurveArena/TimeCurveArenaView.tsx) | Hero clock uses raw `secondsRemaining` from `useTimecurveHeroTimer` | **Must branch** for `saleStartPending` like Simple. | | [`TimeCurveSections.tsx`](frontend/src/pages/timecurve/TimeCurveSections.tsx) (Arena standings / raw accordion) | `deadline` row + `secondsRemaining` seconds | Should show **open vs round** semantics consistently in pre-start. | | [`TimeCurveLiveCharts.tsx`](frontend/src/pages/timecurve/TimeCurveLiveCharts.tsx) | Chart window uses `saleStartSec` / `deadlineSec` | Confirm no misleading “progress” before open. | | [`TimeCurveProtocolPage.tsx`](frontend/src/pages/TimeCurveProtocolPage.tsx) | Read-only `deadline` (and related) | Optional: add **`saleStart`** row if missing; informational, not a ticking hero. | | [`TimecurveBuyModals.tsx`](frontend/src/pages/timecurve/TimecurveBuyModals.tsx) | Buy detail `new_deadline` | TX-scoped; unchanged unless copy audit requests. | | Cooldown strings (`formatCountdown` in Simple / Arena) | Wallet buy cooldown | Not sale open; leave as-is unless confusing next to new label. | ## Data / information needed 1. **Onchain (source of truth)** - `TimeCurve.saleStart()` — epoch set by `startSaleAt` ([#114](https://gitlab.com/PlasticDigits/yieldomega/-/issues/114)). - `TimeCurve.deadline()` — round end; moves with buys after open. - `TimeCurve.ended()`. 2. **Indexer-anchored clock (UX consistency, [#48](https://gitlab.com/PlasticDigits/yieldomega/-/issues/48))** - Today: `block_timestamp_sec` + skew in `useTimecurveHeroTimer`. - Optional extension: **`sale_start_sec`** on `GET /v1/timecurve/chain-timer` so the **same snapshot** carries both targets and avoids RPC/indexer drift for the **target** timestamps (deadline already comes from indexer poll). 3. **Phase** - Reuse [`derivePhase`](frontend/src/pages/timecurve/timeCurveSimplePhase.ts) / `ledgerSecIntForPhase` so **no** page invents a separate “is it open?” rule. ## Proposed indexer changes (optional but recommended) **File:** [`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs) - Add one **`saleStart()`** `eth_call` in the same `block_id` batch as `deadline`, `timerCapSec`, `ended`, `podium`. - Extend `ChainTimerSnapshot` JSON with **`sale_start_sec`** (decimal string; **`"0"`** when not scheduled — matches contract). - Bump API **`x-schema-version`** in [`indexer/src/api.rs`](indexer/src/api.rs) per project convention; document in [`docs/indexer/design.md`](docs/indexer/design.md) if applicable. **Frontend:** extend [`TimecurveChainTimer`](frontend/src/lib/indexerApi.ts) type + [`snapshotFromIndexerChainTimer`](frontend/src/pages/timecurve/useTimecurveHeroTimer.ts) to parse `sale_start_sec`. **Alternative (minimal indexer):** keep indexer payload as-is; read `saleStart` only via wagmi / `useReadContracts` and combine with **`chainNowSec`** from `useTimecurveHeroTimer` — satisfies [#48] clock alignment but **two sources** for target times; document tradeoff. ## Proposed frontend changes ### Core logic (refactor) 1. **Single derived “hero display seconds”** - Input: `phase` (or booleans), `saleStartSec`, `deadlineSec`, `chainNowSec` (from hero hook), fallbacks from [`ledgerSecIntForPhase`](frontend/src/pages/timecurve/timeCurveSimplePhase.ts). - Output: - `saleStartPending` → `max(0, saleStartSec − floor(chainNow))` - `saleActive` / `saleExpiredAwaitingEnd` → existing `max(0, deadline − chainNow)` (from indexer deadline snapshot) - other phases → `undefined` or `0` per existing UX **Candidate homes (pick one in implementation):** - Extend [`useTimecurveHeroTimer`](frontend/src/pages/timecurve/useTimecurveHeroTimer.ts) to accept `saleStartSec` + phase and return `displaySecondsRemaining`; **or** - New small helper in `timeCurveSimplePhase.ts` / `formatTimer.ts` used by both `useTimeCurveSaleSession` and `useTimeCurveArenaModel`. 2. **Replace Arena’s raw `secondsRemaining`** for the **big digit display** with the derived value above; keep **internal** uses (e.g. timer extension preview, `timerCapSec - secondsRemaining`) on **`deadline − chainNow` only when sale is live** — pre-start should not feed “live round” math. 3. **Copy** - Pre-start hero label: **"TimeCurve Opens In"** (visible title / eyebrow / `aria-live` string as appropriate). - Align [`phaseNarrative`](frontend/src/pages/timecurve/timeCurveSimplePhase.ts) / [`TimeCurveSubnav`](frontend/src/pages/timecurve/TimeCurveSubnav.tsx) hints if they duplicate timer meaning. ### Files likely to touch - [`frontend/src/pages/timecurve/useTimecurveHeroTimer.ts`](frontend/src/pages/timecurve/useTimecurveHeroTimer.ts) - [`frontend/src/pages/timecurve/useTimeCurveSaleSession.ts`](frontend/src/pages/timecurve/useTimeCurveSaleSession.ts) - [`frontend/src/pages/TimeCurveSimplePage.tsx`](frontend/src/pages/TimeCurveSimplePage.tsx) - [`frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx`](frontend/src/pages/timeCurveArena/useTimeCurveArenaModel.tsx) - [`frontend/src/pages/timeCurveArena/TimeCurveArenaView.tsx`](frontend/src/pages/timeCurveArena/TimeCurveArenaView.tsx) - [`frontend/src/pages/timecurve/TimeCurveTimerHero.tsx`](frontend/src/pages/timecurve/TimeCurveTimerHero.tsx) (optional: props for title / eyebrow) - [`frontend/src/pages/timecurve/timeCurveSimplePhase.ts`](frontend/src/pages/timecurve/timeCurveSimplePhase.ts) (+ [`timeCurveSimplePhase.test.ts`](frontend/src/pages/timecurve/timeCurveSimplePhase.test.ts)) - [`frontend/src/lib/indexerApi.ts`](frontend/src/lib/indexerApi.ts) - [`frontend/src/pages/timecurve/TimeCurveSections.tsx`](frontend/src/pages/timecurve/TimeCurveSections.tsx) - [`frontend/e2e/timecurve.spec.ts`](frontend/e2e/timecurve.spec.ts) (if selectors / copy change) - Docs: [`docs/frontend/timecurve-views.md`](docs/frontend/timecurve-views.md) (scheduled start / #114 section), [`docs/testing/invariants-and-business-logic.md`](docs/testing/invariants-and-business-logic.md) if new invariant ID **Indexer (if extended):** [`indexer/src/chain_timer.rs`](indexer/src/chain_timer.rs), [`indexer/src/api.rs`](indexer/src/api.rs), [`indexer/tests/integration_stage2.rs`](indexer/tests/integration_stage2.rs) ## Acceptance criteria - [ ] When **`derivePhase` === `saleStartPending`**, **Simple** and **Arena** hero countdown digits both equal **`max(0, saleStart − indexerAnchoredChainNow)`** (within 1s tolerance vs `cast call` on `saleStart` + head block). - [ ] Prominent UI copy includes the exact phrase **"TimeCurve Opens In"** (or equivalent screen-reader text) **during `saleStartPending`** on both routes. - [ ] When the sale becomes live, the same hero components **without full page reload** switch to counting down to **`deadline`** with no stale “opens in” label. - [ ] No regression to [#48](https://gitlab.com/PlasticDigits/yieldomega/-/issues/48): phase strip / buy gating must not disagree with hero digits because of split clocks when indexer snapshot exists. - [ ] If **`VITE_INDEXER_URL`** is unset or chain-timer unreachable, behavior matches existing fallback policy (document any intentional difference). ## Verification criteria 1. **Unit tests:** add cases for “pre-start display seconds” vs “live seconds” in `timeCurveSimplePhase.test.ts` or a dedicated helper test file; include edge: `saleStartSec == 0` (loading), `ledgerSecInt` just before/after `saleStart`. 2. **Local Anvil:** use `YIELDOMEGA_DEV_SALE_START_DELAY_SEC` / stack script behavior from [#114](https://gitlab.com/PlasticDigits/yieldomega/-/issues/114) so `saleStart` is in the future; open **`/timecurve`** and **`/timecurve/arena`** side-by-side — digits and **"TimeCurve Opens In"** must match. 3. **Cross-check:** `cast call` (or read contract UI) for `saleStart`, `deadline`, latest `block.timestamp` vs displayed remaining seconds. 4. **Indexer optional path:** if `sale_start_sec` is added, assert JSON field present and matches RPC at same `read_block_number`. 5. **Regression:** post-open, buy once and confirm deadline extension still updates hero; WarBow / cooldown copy unchanged. ## Checklists ### Implementation - [ ] Identify every user-visible countdown on TimeCurve routes (table above + grep for `formatLaunchCountdown`, `formatCountdown`, `secondsRemaining` in `frontend/src/pages/timecurve/` and `timeCurveArena/`). - [ ] Implement shared derived countdown API; remove Arena’s incorrect use of `deadline - now` during pre-start. - [ ] Add **"TimeCurve Opens In"** to Simple + Arena hero surfaces; update `aria-live` strings. - [ ] (Optional) Extend indexer `chain-timer` with `sale_start_sec`; update `indexerApi` + hook. - [ ] Update docs / invariants cross-links. - [ ] Run `npm run typecheck`, `npm test`, targeted e2e if copy/selectors change. ### Code review - [ ] Single phase machine: no new `sale not live` branching outside `derivePhase` / `ledgerSecIntForPhase`. - [ ] Timer extension / `timerCapSec` math uses **live** deadline semantics only. - [ ] Schema/version bump for indexer if API extended. - [ ] No drive-by refactors unrelated to timers. ### QA (manual) - [ ] Pre-start: Simple + Arena show **same** remaining sec to `saleStart`. - [ ] At `saleStart` boundary: both flip to round countdown within ~1s. - [ ] Indexer offline / backoff ([#96](https://gitlab.com/PlasticDigits/yieldomega/-/issues/96)): no crash; stale/offline messaging unchanged or improved. - [ ] `prefers-reduced-motion`: sparks/animations still respect global rules. - [ ] Mobile width: digit layout stable (tabular nums). ## Related - [#114 — `startSaleAt` / scheduled sale](https://gitlab.com/PlasticDigits/yieldomega/-/issues/114) - [#48 — indexer-anchored hero timer + phase clock](https://gitlab.com/PlasticDigits/yieldomega/-/issues/48) - [`docs/frontend/timecurve-views.md` — Scheduled on-chain starts](https://github.com/PlasticDigits/yieldomega/blob/main/docs/frontend/timecurve-views.md) (repository path; adjust if using GitLab mirror)
PlasticDigits commented 2026-05-03 10:47:53 +00:00 (Migrated from gitlab.com)

marked this issue as related to #48

marked this issue as related to #48
PlasticDigits commented 2026-05-03 10:47:54 +00:00 (Migrated from gitlab.com)

marked this issue as related to #114

marked this issue as related to #114
PlasticDigits commented 2026-05-03 13:03:43 +00:00 (Migrated from gitlab.com)

Implemented (7ceb43a)

Pre-open (saleStartPending) hero countdown is unified on Simple + Arena: digits use max(0, saleStart − floor(chainNow)) with the same indexer-anchored clock as #48. Indexer GET /v1/timecurve/chain-timer now includes sale_start_sec (same block as deadline_sec); API schema 1.11.0. Live phases still use deadline − chainNow; timer-cap / extension preview uses live deadline seconds only.

Copy: prominent TimeCurve Opens In (Simple section title, Arena rail label, a11y).

@brouie Please verify when you have a moment (issue stays open).

Checklist for verification

  • Pre-start: With scheduled saleStart in the future (e.g. local stack + YIELDOMEGA_DEV_SALE_START_DELAY_SEC), open /timecurve and /timecurve/arena side-by-side — hero digits match within ~1s.
  • Indexer: curl chain-timer (or browser) shows sale_start_sec matching cast call saleStart() at the same read_block_number.
  • Flip: When now crosses saleStart (no full reload), both routes switch to round countdown + Time Remaining / live semantics; no stale Opens In label.
  • Regression: After open, buy once — deadline extension + timerCap preview still sane; WarBow / wallet cooldown unchanged.
  • Offline indexer: No crash; existing fallback when chain-timer unavailable (#96).
  • Schema: x-schema-version 1.11.0 on indexer responses after deploy.

Docs / invariants: timecurve-views.md §115, invariants-and-business-logic.md §115, skills/README.md, guardrails #38.

## Implemented ([`7ceb43a`](https://gitlab.com/PlasticDigits/yieldomega/-/commit/7ceb43a)) Pre-open (**`saleStartPending`**) hero countdown is unified on **Simple** + **Arena**: digits use `max(0, saleStart − floor(chainNow))` with the same indexer-anchored clock as **#48**. **Indexer** `GET /v1/timecurve/chain-timer` now includes **`sale_start_sec`** (same block as `deadline_sec`); API schema **1.11.0**. Live phases still use `deadline − chainNow`; timer-cap / extension preview uses **live** deadline seconds only. **Copy:** prominent **TimeCurve Opens In** (Simple section title, Arena rail label, a11y). @brouie Please verify when you have a moment (issue stays open). ### Checklist for verification - [ ] **Pre-start:** With scheduled `saleStart` in the future (e.g. local stack + `YIELDOMEGA_DEV_SALE_START_DELAY_SEC`), open `/timecurve` and `/timecurve/arena` side-by-side — hero digits match within ~1s. - [ ] **Indexer:** `curl` chain-timer (or browser) shows `sale_start_sec` matching `cast call` `saleStart()` at the same `read_block_number`. - [ ] **Flip:** When `now` crosses `saleStart` (no full reload), both routes switch to round countdown + **Time Remaining** / live semantics; **no** stale **Opens In** label. - [ ] **Regression:** After open, buy once — deadline extension + **timerCap** preview still sane; WarBow / wallet cooldown unchanged. - [ ] **Offline indexer:** No crash; existing fallback when chain-timer unavailable (`#96`). - [ ] **Schema:** `x-schema-version` **1.11.0** on indexer responses after deploy. **Docs / invariants:** [`timecurve-views.md` §115](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/frontend/timecurve-views.md), [`invariants-and-business-logic.md` §115](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md), [`skills/README.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/skills/README.md), guardrails **#38**.
Brouie commented 2026-05-04 00:52:52 +00:00 (Migrated from gitlab.com)

verified on 7ceb43a against fresh deploy walk (carried from #114 verification today, same stack). schema/indexer: x-schema-version: 1.11.0 on /v1/timecurve/chain-timer; sale_start_sec=1777855603 matches cast call saleStart() exactly + deadline_sec=1777942003 (delta 86400s = initialTimerSec). pre-start hero on /timecurve: PRE-LAUNCH badge + "TimeCurve Opens In" copy + 9:46 countdown ticking down toward saleStart, Buy CHARM card disabled with "Coming soon". flip to live (anvil_increaseTime 600s): /timecurve transitions to SALE LIVE + "Time Left" + 23:51 deadline countdown; /timecurve/arena shows SALE LIVE + "TIME REMAINING" + 23:52 countdown — both within ~1s of each other reading the same indexer-anchored clock (drift = wall-clock between page loads). Arena explicitly shows "On-chain deadline(): … · read at block 8" and "Head block.timestamp: …" so the dual reference is right there in UI. no stale "Opens In" copy on either route post-flip. deferred 2 items: regression after a real buy (timer-cap / extension preview sanity) and offline-indexer fallback (#96) — neither is the meat of #115's countdown unification fix; happy to retest if you want, but the chain-timer + phase-swap + copy + parity surfaces are the core ones and they're clean. all 4 core items pass.

verified on `7ceb43a` against fresh deploy walk (carried from #114 verification today, same stack). **schema/indexer**: `x-schema-version: 1.11.0` on /v1/timecurve/chain-timer; `sale_start_sec=1777855603` matches `cast call saleStart()` exactly + `deadline_sec=1777942003` (delta 86400s = initialTimerSec). **pre-start hero on /timecurve**: PRE-LAUNCH badge + "TimeCurve Opens In" copy + 9:46 countdown ticking down toward saleStart, Buy CHARM card disabled with "Coming soon". **flip to live (anvil_increaseTime 600s)**: /timecurve transitions to SALE LIVE + "Time Left" + 23:51 deadline countdown; /timecurve/arena shows SALE LIVE + "TIME REMAINING" + 23:52 countdown — both within ~1s of each other reading the same indexer-anchored clock (drift = wall-clock between page loads). Arena explicitly shows "On-chain deadline(): … · read at block 8" and "Head block.timestamp: …" so the dual reference is right there in UI. no stale "Opens In" copy on either route post-flip. **deferred 2 items**: regression after a real buy (timer-cap / extension preview sanity) and offline-indexer fallback (#96) — neither is the meat of #115's countdown unification fix; happy to retest if you want, but the chain-timer + phase-swap + copy + parity surfaces are the core ones and they're clean. all 4 core items pass.
Brouie commented 2026-05-04 02:42:59 +00:00 (Migrated from gitlab.com)

mentioned in issue #96

mentioned in issue #96
Brouie (Migrated from gitlab.com) closed this issue 2026-05-04 03:10:27 +00:00
PlasticDigits commented 2026-05-04 04:22:29 +00:00 (Migrated from gitlab.com)

mentioned in issue #124

mentioned in issue #124
PlasticDigits commented 2026-05-21 04:38:41 +00:00 (Migrated from gitlab.com)

mentioned in issue #227

mentioned in issue #227
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
code/yieldomega#115
No description provided.