From de3eb28d0aa3e29a958d8dae8c84193e541a3ad8 Mon Sep 17 00:00:00 2001 From: jagdeep sidhu Date: Sat, 25 Apr 2026 16:59:17 -0700 Subject: [PATCH] refactor(api): standardize public route casing on lowercase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Companion to syscoin/sysnode-backend#23. The historical canonical URL for the public sysnode-backend endpoints is lowercase: syshub still calls `https://syscoin.dev/mnstats` lowercase, and `/govlist` was already lowercase. The camelCase only appeared because the backend route files registered `router.get("/mnStats", …)`; Express's default case-insensitive routing hid the drift across both ends. Pin the SPA on the lowercase canonical so future readers and external integrators see one consistent shape: - src/lib/api.js: client.get('/mnstats'), client.get('/mncount'). - README data-source layout block updated. - Comments in NewProposal.{js,test.js} and governanceWindow.{js,test.js} refreshed to use the lowercase path names. (Behaviourally these are documentation refs; the request URL change is in api.js.) Backwards compat: the backend's case-insensitive default routing AND the case-insensitive `~*` regex match in the bundled nginx example (syscoin/sysnode-backend#23) mean any in-flight build still using camelCase keeps working during a rolling deploy. New code paths land on lowercase from the start. 880 jest tests pass. Made-with: Cursor --- README.md | 4 ++-- src/lib/api.js | 6 +++--- src/lib/governanceWindow.js | 2 +- src/lib/governanceWindow.test.js | 4 ++-- src/pages/NewProposal.js | 14 +++++++------- src/pages/NewProposal.test.js | 24 ++++++++++++------------ 6 files changed, 27 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index a98afd8..de0febb 100644 --- a/README.md +++ b/README.md @@ -36,8 +36,8 @@ https://sysnode.info/ -> SPA https://sysnode.info/auth/* -> backend https://sysnode.info/vault/* -> backend https://sysnode.info/gov/* -> backend -https://sysnode.info/mnStats -> backend -https://sysnode.info/mnCount -> backend +https://sysnode.info/mnstats -> backend +https://sysnode.info/mncount -> backend https://sysnode.info/govlist -> backend ``` diff --git a/src/lib/api.js b/src/lib/api.js index dbb4472..093682f 100644 --- a/src/lib/api.js +++ b/src/lib/api.js @@ -1,7 +1,7 @@ import axios from 'axios'; // Base URL for the anonymous public sysnode-backend endpoints -// (`/mnStats`, `/mnCount`, `/govlist`). Kept in lockstep with the +// (`/mnstats`, `/mncount`, `/govlist`). Kept in lockstep with the // authenticated client in `./apiClient.js` so a single build-time // override (`REACT_APP_API_BASE`) retargets BOTH surfaces at once. // @@ -33,12 +33,12 @@ const client = axios.create({ }); export async function fetchNetworkStats() { - const response = await client.get('/mnStats'); + const response = await client.get('/mnstats'); return response.data; } export async function fetchNodeHistory() { - const response = await client.get('/mnCount'); + const response = await client.get('/mncount'); return response.data; } diff --git a/src/lib/governanceWindow.js b/src/lib/governanceWindow.js index b13e46f..8ddff88 100644 --- a/src/lib/governanceWindow.js +++ b/src/lib/governanceWindow.js @@ -278,7 +278,7 @@ export function computeProposalWindow({ return { startEpoch, endEpoch, anchor, padding }; } -// Extract the next-superblock epoch (seconds) from the /mnStats +// Extract the next-superblock epoch (seconds) from the /mnstats // response. The backend exposes a numeric `superblock_next_epoch_sec` // field (see sysnode-backend services/calculations.js). We never // parse the human-readable `superblock_date` string — it's formatted diff --git a/src/lib/governanceWindow.test.js b/src/lib/governanceWindow.test.js index 987ee91..f1dcd18 100644 --- a/src/lib/governanceWindow.test.js +++ b/src/lib/governanceWindow.test.js @@ -50,7 +50,7 @@ describe('SUPERBLOCK_CYCLE_SEC', () => { }); // Codex PR20 round 4 P1: the prepare-time drift check must not -// treat sub-SB /mnStats re-estimates as a superblock rotation. +// treat sub-SB /mnstats re-estimates as a superblock rotation. describe('anchorsAreSameSuperblock', () => { const NOW = 1_800_000_000; const ANCHOR = NOW + 10 * 86400; @@ -516,7 +516,7 @@ describe('nextSuperblockEpochSecFromStats', () => { ).toBeNull(); }); - // Codex PR20 P1: the backend's /mnStats feed can lag for a + // Codex PR20 P1: the backend's /mnstats feed can lag for a // window between the real next superblock landing and // sysMain.js refreshing its cache. If we were to accept that // stale (past) timestamp as a valid anchor, the wizard would diff --git a/src/pages/NewProposal.js b/src/pages/NewProposal.js index b632a07..8ad4ef4 100644 --- a/src/pages/NewProposal.js +++ b/src/pages/NewProposal.js @@ -189,7 +189,7 @@ export default function NewProposal() { // toast-like feedback. const [draftSavedAt, setDraftSavedAt] = useState(0); - // Live next-superblock anchor. We fetch the backend /mnStats feed + // Live next-superblock anchor. We fetch the backend /mnstats feed // on mount and extract `superblock_stats.superblock_next_epoch_sec` // so that `computeProposalWindow` can align the derived start/end // epochs to the real chain. Loading / error states gate the @@ -199,7 +199,7 @@ export default function NewProposal() { const [nextSuperblockSec, setNextSuperblockSec] = useState(null); const [statsError, setStatsError] = useState(null); const [statsLoading, setStatsLoading] = useState(true); - // Monotonic counter tracking the "latest issued /mnStats request". + // Monotonic counter tracking the "latest issued /mnstats request". // Each refreshStats invocation takes a snapshot of its own id and // only mutates anchor state if that id is still current when the // response resolves. Prevents out-of-order responses from clobbering @@ -272,7 +272,7 @@ export default function NewProposal() { return () => clearInterval(id); }, []); - // True only when /mnStats gave us a future superblock anchor. + // True only when /mnstats gave us a future superblock anchor. // Used consistently by WindowPreview, the Prepare-button gate, // and the ReviewStep schedule so all three flip together the // instant the cached anchor goes stale. Mere truthiness is @@ -712,7 +712,7 @@ export default function NewProposal() { // the stats-unavailable banner, clear the cached anchor // so Prepare stays disabled until refreshStats() recovers. // (b) fetch returns a stale or missing anchor - // (next_SB epoch <= now) → same as (a). The /mnStats + // (next_SB epoch <= now) → same as (a). The /mnstats // source occasionally lags a few blocks behind the tip // and we refuse to submit against a backward-pointing // anchor for the same reason. @@ -735,7 +735,7 @@ export default function NewProposal() { // // Codex PR20 round 3 P2: the wall-clock cutoff used to validate // the refreshed anchor must be read AFTER the fetch resolves, - // not before. /mnStats is a real network RTT (plus jsdom / + // not before. /mnstats is a real network RTT (plus jsdom / // proxy / slow-node delays in practice) and can straddle the // actual superblock transition; in that window an anchor that // was strictly future at pre-await time can already be in the @@ -776,7 +776,7 @@ export default function NewProposal() { } // Compare the refreshed anchor against the cached one the // user just reviewed. We CANNOT use strict equality — the - // backend's /mnStats recomputes `superblock_next_epoch_sec` + // backend's /mnstats recomputes `superblock_next_epoch_sec` // every sysMain tick (20 s) as `now + diffBlock * // avgBlockTime`, so the value drifts by seconds/minutes // between fetches even when the same upcoming superblock is @@ -1632,7 +1632,7 @@ function ReviewStep({ form.paymentCount ); // Only render the projected schedule when we have a live next-SB - // anchor from /mnStats. `derivedWindow` alone is insufficient: + // anchor from /mnstats. `derivedWindow` alone is insufficient: // computeProposalWindow falls back to `now + cycle` when the anchor // is missing/stale, which would paint a plausible-looking list of // payout dates that don't match the chain. WindowPreview already diff --git a/src/pages/NewProposal.test.js b/src/pages/NewProposal.test.js index e062b38..fd27213 100644 --- a/src/pages/NewProposal.test.js +++ b/src/pages/NewProposal.test.js @@ -71,7 +71,7 @@ import { proposalService } from '../lib/proposalService'; /* eslint-enable import/first */ // Stable next-superblock anchor captured fresh per-test in beforeEach -// (see below). The wizard now fetches /mnStats BOTH on mount AND at +// (see below). The wizard now fetches /mnstats BOTH on mount AND at // Prepare time and compares the two — if they differ it assumes the // chain advanced a cycle while the wizard was open and forces a // re-review instead of submitting. A mock that recomputes @@ -170,7 +170,7 @@ describe('NewProposal wizard', () => { jest.clearAllMocks(); // Snapshot a stable next-SB anchor at test start so both the // mount-time and prepare-time fetchNetworkStats calls return - // the same value (same rationale as in production: /mnStats + // the same value (same rationale as in production: /mnstats // reports the same pre-computed SB epoch across rapid calls). currentStableNextSb = Math.floor(Date.now() / 1000) + 30 * 86400; fetchNetworkStats.mockImplementation(defaultNetworkStatsResolver); @@ -780,7 +780,7 @@ describe('NewProposal wizard', () => { ); test( - 'Prepare fails closed when the pre-submit /mnStats refresh throws (Codex round 2 P2)', + 'Prepare fails closed when the pre-submit /mnstats refresh throws (Codex round 2 P2)', async () => { // The wizard refreshes the next-SB anchor right before // submitting. If that fetch errors out, we must NOT fall @@ -799,7 +799,7 @@ describe('NewProposal wizard', () => { fireEvent.click(screen.getByTestId('wizard-next')); expect(screen.getByTestId('wizard-panel-review')).toBeInTheDocument(); - // Now break the /mnStats endpoint for the prepare-time + // Now break the /mnstats endpoint for the prepare-time // refresh. The mount-time fetch already succeeded with the // stable anchor from beforeEach. fetchNetworkStats.mockRejectedValueOnce( @@ -829,10 +829,10 @@ describe('NewProposal wizard', () => { ); test( - 'Prepare fails closed when the pre-submit /mnStats refresh returns a stale (past) anchor', + 'Prepare fails closed when the pre-submit /mnstats refresh returns a stale (past) anchor', async () => { // Same fail-closed behaviour as the throw case: a lagging - // /mnStats feed that still returns a positive but past + // /mnstats feed that still returns a positive but past // timestamp must not let us submit. The mount fetch used // the stable future anchor from beforeEach; we corrupt only // the prepare-time refresh. @@ -869,12 +869,12 @@ describe('NewProposal wizard', () => { ); test( - 'Prepare fails closed when /mnStats resolves across the SB boundary (anchor future at pre-await, past at post-await) (Codex round 3 P2)', + 'Prepare fails closed when /mnstats resolves across the SB boundary (anchor future at pre-await, past at post-await) (Codex round 3 P2)', async () => { // Codex PR20 round 3 P2: the previous implementation captured // `nowSec = Math.floor(Date.now() / 1000)` BEFORE awaiting // fetchNetworkStats() and reused it to validate the refreshed - // anchor. /mnStats is a real network RTT and can straddle + // anchor. /mnstats is a real network RTT and can straddle // wall-clock boundaries — including, at a SB transition, the // actual superblock. In that case an anchor that was strictly // future at pre-await time is already in the past by the time @@ -992,7 +992,7 @@ describe('NewProposal wizard', () => { fireEvent.click(screen.getByTestId('wizard-next')); expect(screen.getByTestId('wizard-panel-review')).toBeInTheDocument(); - // Next /mnStats call returns a DIFFERENT future anchor (one + // Next /mnstats call returns a DIFFERENT future anchor (one // superblock past the cached one). Subsequent calls return // the same drifted value so the retry click sees a stable // state. @@ -1074,7 +1074,7 @@ describe('NewProposal wizard', () => { fireEvent.click(screen.getByTestId('wizard-next')); expect(screen.getByTestId('wizard-panel-review')).toBeInTheDocument(); - // Next /mnStats call returns a slightly drifted anchor + // Next /mnstats call returns a slightly drifted anchor // (60 s forward). Well within the cycle/2 tolerance, so // the wizard must treat it as "same SB, just a fresher // estimate" and proceed to prepare. @@ -1117,7 +1117,7 @@ describe('NewProposal wizard', () => { ); test( - 'Review step suppresses the projected schedule when /mnStats anchor is unavailable (Codex round 5 P2)', + 'Review step suppresses the projected schedule when /mnstats anchor is unavailable (Codex round 5 P2)', async () => { // Regression: computeProposalWindow has an internal // "stale anchor" fallback (anchor = now + cycle) so that @@ -1132,7 +1132,7 @@ describe('NewProposal wizard', () => { // in lockstep with WindowPreview and only render when we // have a real live anchor. // - // Setup: stub /mnStats to return a response that + // Setup: stub /mnstats to return a response that // nextSuperblockEpochSecFromStats rejects (missing // superblock_next_epoch_sec field). Wizard state lands at // `nextSuperblockSec = null, statsError != null`. 3-month