feat: add us debt screen #2

Open
PMK wants to merge 18 commits from PMK/btclock_v4:feat/usdebt_screen into main
First-time contributor
No description provided.
feat: add us debt screen
Some checks failed
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
d6de710d33
Merge branch 'main' into feat/usdebt_screen
Some checks failed
Host tests / host_tests (pull_request) Successful in 34s
Host tests / coverage (pull_request) Successful in 35s
Lint / format (pull_request) Failing after 36s
Lint / tidy (pull_request) Successful in 1m57s
Host tests / sanitize (pull_request) Successful in 3m58s
345d1fc084
fix: add includes
Some checks failed
Host tests / host_tests (pull_request) Successful in 34s
Host tests / coverage (pull_request) Successful in 33s
Lint / format (pull_request) Failing after 35s
Lint / tidy (pull_request) Successful in 1m58s
Host tests / sanitize (pull_request) Successful in 4m4s
2442275596
fix(us_debt): toggle big number
Some checks failed
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
478a6ddedf
PMK changed title from WIP: feat: add us debt screen to feat: add us debt screen 2026-06-06 15:18:16 +00:00
Merge branch 'main' into feat/usdebt_screen
Some checks failed
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
16367ece6d
fix(us_debt): add precise big debt number
Some checks failed
Host tests / host_tests (pull_request) Successful in 2m3s
Host tests / sanitize (pull_request) Successful in 4m27s
Host tests / coverage (pull_request) Successful in 2m50s
Lint / format (pull_request) Failing after 1m14s
Lint / tidy (pull_request) Successful in 4m5s
8e4bd0553d
Merge branch 'main' into feat/usdebt_screen
Some checks failed
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
0c194c65eb
chore: format code
All checks were successful
Host tests / host_tests (pull_request) Successful in 2m12s
Host tests / sanitize (pull_request) Successful in 4m51s
Host tests / coverage (pull_request) Successful in 2m56s
Lint / format (pull_request) Successful in 1m16s
Lint / tidy (pull_request) Successful in 3m40s
189001a0dc
Owner

PR Reviewer Guide 🔍

(Review updated until commit d5333a0a79)

Here are some key observations to aid the review process:

⏱️ Estimated effort to review: 3 🔵🔵🔵
🧪 PR contains tests
🔒 No security concerns identified
 Recommended focus areas for review

Floating-point precision

In the "precise mode" fallback path (when us_debt_exact is nullopt but us_debt_t is set), the code computes *us_debt_t * 1e12 and casts to uint64_t. Since us_debt_t (e.g. 39.2) is not exactly representable in IEEE 754 double, the multiplication may produce a value like 39199999999999.998… which truncates to 39199999999999 instead of the expected 39200000000000. In normal operation this fallback never triggers because us_debt_t and us_debt_exact are always populated together in PollOnce(), but if they ever diverge (e.g., partial snapshot merge edge case), the displayed precise number could be wrong by one in the last digit.

const double raw =
    has_debt ? ((us_debt_exact && *us_debt_exact > 0.0) ? *us_debt_exact
                                                        : *us_debt_t * 1e12)
             : 0.0;
const uint64_t debt = has_debt ? static_cast<uint64_t>(raw) : 0;
## PR Reviewer Guide 🔍 #### (Review updated until commit https://git.btclock.dev/btclock/btclock_v4/commit/d5333a0a7932ab92e175deb2ddc4d5e61c7ab4a0) Here are some key observations to aid the review process: <table> <tr><td>⏱️&nbsp;<strong>Estimated effort to review</strong>: 3 🔵🔵🔵⚪⚪</td></tr> <tr><td>🧪&nbsp;<strong>PR contains tests</strong></td></tr> <tr><td>🔒&nbsp;<strong>No security concerns identified</strong></td></tr> <tr><td>⚡&nbsp;<strong>Recommended focus areas for review</strong><br><br> <details><summary><a href='https://git.btclock.dev/btclock/btclock_v4/src/branch/feat/usdebt_screen/main/screens/us_debt.cpp#L96-L100'><strong>Floating-point precision</strong></a> In the "precise mode" fallback path (when `us_debt_exact` is nullopt but `us_debt_t` is set), the code computes `*us_debt_t * 1e12` and casts to `uint64_t`. Since `us_debt_t` (e.g. 39.2) is not exactly representable in IEEE 754 double, the multiplication may produce a value like 39199999999999.998… which truncates to 39199999999999 instead of the expected 39200000000000. In normal operation this fallback never triggers because `us_debt_t` and `us_debt_exact` are always populated together in `PollOnce()`, but if they ever diverge (e.g., partial snapshot merge edge case), the displayed precise number could be wrong by one in the last digit. </summary> ```c++ const double raw = has_debt ? ((us_debt_exact && *us_debt_exact > 0.0) ? *us_debt_exact : *us_debt_t * 1e12) : 0.0; const uint64_t debt = has_debt ? static_cast<uint64_t>(raw) : 0; ``` </details> </td></tr> </table>
Owner

PR Code Suggestions

No code suggestions found for the PR.

## PR Code Suggestions ✨ No code suggestions found for the PR.
Owner

PR Code Suggestions

No code suggestions found for the PR.

## PR Code Suggestions ✨ No code suggestions found for the PR.
Owner

Persistent review updated to latest commit d5333a0a79

**[Persistent review](https://git.btclock.dev/btclock/btclock_v4/pulls/2#issuecomment-1751)** updated to latest commit https://git.btclock.dev/btclock/btclock_v4/commit/d5333a0a7932ab92e175deb2ddc4d5e61c7ab4a0
Owner

🤖 Code Review — US Debt screen (feat/usdebt_screen)

Manual review by Claude (read the full head content of the new us_debt component + the integration points, not just the diff). Overall this is a clean, well-structured addition — solid task lifecycle, good RAII, and a sensible compact/precise display split. A few things worth addressing before merge, ordered by severity.


🟠 Medium

1. Dead branch in FormatUsDebtStringmain/screens/panel_texts.cpp#L946-L949

} else if (value_p < 10.0) {
  std::snprintf(buf, sizeof(buf), "$%.1fP", value_p);
} else {
  std::snprintf(buf, sizeof(buf), "$%.1fP", value_p);   // identical to the branch above
}

The value_p < 10.0 branch and the final else produce the exact same format string, so the < 10.0 test is dead. Given the < 1.0 branch uses "$%.2fP", the intent was almost certainly "$%.2fP" here too (extra precision for single-digit quadrillions). Either restore the intended %.2f or drop the redundant branch.

2. Triplicated "dollars + 3-digit grouping" logic. The derivation

const double raw = (us_debt_exact && *us_debt_exact > 0.0) ? *us_debt_exact
                                                           : *us_debt_t * 1e12;

plus the uint64 → 3-char groups formatting is duplicated across three places that must stay in lockstep:

A format tweak or bug-fix now needs editing in three spots — high divergence risk. Suggest two small shared helpers, e.g. UsDebtDollars(const DataSnapshot&) -> uint64_t and GroupDigits(uint64_t) -> std::vector<std::string>, reused by source/screen/manager.


🟡 Low

3. NVS read on every ShouldRender callscreen_manager.cpp#L700-L707

btclock::Prefs detect_prefs(btclock::prefs::kSettingsNs);
const bool big = btclock::settings::ReadBool(detect_prefs, btclock::prefs::kUsDebtBigChar);

This opens an NVS handle and reads from flash each time ShouldRender runs, whereas the other screen cases just compare cached values. The us_debt_big_char flag is already read once into RenderPrefs (rp.us_debt_big_char). Reuse the cached value here to avoid per-tick flash I/O and to guarantee ShouldRender and Render agree within the same cycle.

4. Float fallback precisionus_debt.cpp#L96-L100 (and the two duplicates)

The *us_debt_t * 1e12 fallback is doubly lossy: us_debt_t is already rounded to 0.1T (±~50 billion), and the double → uint64 cast can truncate (e.g. 39.2 * 1e12 → 39199999999999). It's effectively unreachable today because PollOnce() always sets us_debt_exact alongside us_debt_t, so this is defensive-only — but if the two ever diverge (e.g. a partial snapshot merge), the precise display would be wrong. When using us_debt_exact the path is exact (integer part < 2^53), so the fix is just to make the fallback's lossiness explicit or drop it.

5. Task stack size — please verify. us_debt_source.cpp#L127-L129 creates the worker with a 4 KB stack while it runs esp_http_client_perform + cJSON parsing. If TLS terminates on this task (rather than in the proxy transport), 4 KB is tight and a handshake could overflow it. Worth a quick uxTaskGetStackHighWaterMark() check under a real fetch.


Looks good / verified

  • Lifecycle is correct. Stop() sets stop_, joins on the done_ semaphore (≤12 s) and only then clears hub_, so there's no use-after-free / data race on hub_ from PollOnce(). FetchContext frees its buffer via RAII. Nicely done.
  • Response guard. 8 KB cap with a truncated flag prevents unbounded growth; body[size] = '\0' stays within the kMaxResponseBytes + 1 allocation.
  • static_assert(kAgnosticSlots == 11, …) bumped in lockstep with the slot map — good guard against the documented slot regression.
  • Not a bug: the heap_caps_malloc_prefer(kMaxResponseBytes + 1, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_8BIT) call is correct — the 2 is the count of capability arguments (num), not a caps bitmask. It properly prefers SPIRAM and falls back to internal 8-bit RAM. (Flagging explicitly because an automated pass read this as a "wrong caps" bug — it's a false positive.)

🧪 Tests

test_host/test_us_debt_panel_texts.cpp covers the panel-text formatting — good. Note the duplicated render/manager grouping logic (point 2) isn't exercised by those tests; folding it into a shared helper would also make it directly testable.

## 🤖 Code Review — US Debt screen (`feat/usdebt_screen`) Manual review by **Claude** (read the full head content of the new `us_debt` component + the integration points, not just the diff). Overall this is a clean, well-structured addition — solid task lifecycle, good RAII, and a sensible compact/precise display split. A few things worth addressing before merge, ordered by severity. --- ### 🟠 Medium **1. Dead branch in `FormatUsDebtString`** — [`main/screens/panel_texts.cpp#L946-L949`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/screens/panel_texts.cpp#L946-L949) ```cpp } else if (value_p < 10.0) { std::snprintf(buf, sizeof(buf), "$%.1fP", value_p); } else { std::snprintf(buf, sizeof(buf), "$%.1fP", value_p); // identical to the branch above } ``` The `value_p < 10.0` branch and the final `else` produce the **exact same** format string, so the `< 10.0` test is dead. Given the `< 1.0` branch uses `"$%.2fP"`, the intent was almost certainly `"$%.2fP"` here too (extra precision for single-digit quadrillions). Either restore the intended `%.2f` or drop the redundant branch. **2. Triplicated "dollars + 3-digit grouping" logic.** The derivation ```cpp const double raw = (us_debt_exact && *us_debt_exact > 0.0) ? *us_debt_exact : *us_debt_t * 1e12; ``` plus the `uint64 → 3-char groups` formatting is duplicated across **three** places that must stay in lockstep: - [`panel_texts.cpp BuildUsDebt`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/screens/panel_texts.cpp#L858) - [`main/screens/us_debt.cpp RenderUsDebtScreen` + `DebtGroups`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/screens/us_debt.cpp#L20) - [`screen_manager.cpp ShouldRender` (L708) and `Render` (L1019)](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/app/screen_manager.cpp#L700-L715) A format tweak or bug-fix now needs editing in three spots — high divergence risk. Suggest two small shared helpers, e.g. `UsDebtDollars(const DataSnapshot&) -> uint64_t` and `GroupDigits(uint64_t) -> std::vector<std::string>`, reused by source/screen/manager. --- ### 🟡 Low **3. NVS read on every `ShouldRender` call** — [`screen_manager.cpp#L700-L707`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/app/screen_manager.cpp#L700-L707) ```cpp btclock::Prefs detect_prefs(btclock::prefs::kSettingsNs); const bool big = btclock::settings::ReadBool(detect_prefs, btclock::prefs::kUsDebtBigChar); ``` This opens an NVS handle and reads from flash *each time* `ShouldRender` runs, whereas the other screen cases just compare cached values. The `us_debt_big_char` flag is already read once into `RenderPrefs` (`rp.us_debt_big_char`). Reuse the cached value here to avoid per-tick flash I/O and to guarantee `ShouldRender` and `Render` agree within the same cycle. **4. Float fallback precision** — [`us_debt.cpp#L96-L100`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/main/screens/us_debt.cpp#L96-L100) (and the two duplicates) The `*us_debt_t * 1e12` fallback is doubly lossy: `us_debt_t` is already rounded to 0.1T (±~50 billion), and the `double → uint64` cast can truncate (e.g. `39.2 * 1e12 → 39199999999999`). It's effectively unreachable today because `PollOnce()` always sets `us_debt_exact` alongside `us_debt_t`, so this is defensive-only — but if the two ever diverge (e.g. a partial snapshot merge), the precise display would be wrong. When using `us_debt_exact` the path is exact (integer part < 2^53), so the fix is just to make the fallback's lossiness explicit or drop it. **5. Task stack size — please verify.** [`us_debt_source.cpp#L127-L129`](https://git.btclock.dev/btclock/btclock_v4/src/commit/189001a0dca9b0dbade4558ae28f0df04ff1e4a1/components/us_debt/src/us_debt_source.cpp#L127-L129) creates the worker with a **4 KB** stack while it runs `esp_http_client_perform` + cJSON parsing. If TLS terminates on this task (rather than in the proxy transport), 4 KB is tight and a handshake could overflow it. Worth a quick `uxTaskGetStackHighWaterMark()` check under a real fetch. --- ### ✅ Looks good / verified - **Lifecycle is correct.** `Stop()` sets `stop_`, joins on the `done_` semaphore (≤12 s) and only then clears `hub_`, so there's no use-after-free / data race on `hub_` from `PollOnce()`. `FetchContext` frees its buffer via RAII. Nicely done. - **Response guard.** 8 KB cap with a `truncated` flag prevents unbounded growth; `body[size] = '\0'` stays within the `kMaxResponseBytes + 1` allocation. - **`static_assert(kAgnosticSlots == 11, …)`** bumped in lockstep with the slot map — good guard against the documented slot regression. - **Not a bug:** the `heap_caps_malloc_prefer(kMaxResponseBytes + 1, 2, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, MALLOC_CAP_8BIT)` call is correct — the `2` is the **count** of capability arguments (`num`), not a caps bitmask. It properly prefers SPIRAM and falls back to internal 8-bit RAM. (Flagging explicitly because an automated pass read this as a "wrong caps" bug — it's a false positive.) ### 🧪 Tests `test_host/test_us_debt_panel_texts.cpp` covers the panel-text formatting — good. Note the duplicated render/manager grouping logic (point 2) isn't exercised by those tests; folding it into a shared helper would also make it directly testable.
fix: last_rendered_us_debt_big_char_ cached in Render(), reused in ShouldRender()
Some checks failed
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
dc7ac9dccd
fix: revert staging ws url
Some checks failed
Docs site / build (pull_request) Has been cancelled
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
6c0b28256f
docs: fix revert-issues
Some checks failed
Docs site / build (pull_request) Has been cancelled
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
023c44e8c2
docs: whitespace
Some checks failed
Docs site / build (pull_request) Has been cancelled
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
5c8415d517
docs: more revert-issues fixed
Some checks failed
Docs site / build (pull_request) Has been cancelled
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
4b451d03fb
PMK left a comment

Please check my comments.

Please check my comments.
docs/HANDBOOK.md Outdated
Author
First-time contributor

@djuri Can you verify the dataSource if this is done correctly?

@djuri Can you verify the dataSource if this is done correctly?
docs/SETTINGS.md Outdated
@ -168,3 +170,3 @@
| `mempoolInstance` | string | `"mempool.space"` | Base URL of the mempool.space-compatible instance. | Reboot required. |
| `mempoolSecure` | bool | `true` | Use HTTPS/WSS when talking to `mempoolInstance`. | Reboot required. |
| `ceEndpoint` | string | `"ws-staging.btclock.dev"` | Custom-endpoint host when `dataSource=1`. | Reboot required. |
| `ceEndpoint` | string | `"ws.btclock.dev"` | Custom-endpoint host when `dataSource=2/3`. | Reboot required. |
Author
First-time contributor

@djuri Can you verify the dataSource if this is done correctly?

@djuri Can you verify the dataSource if this is done correctly?
@ -19,6 +19,8 @@
// slot 12 + 3k : kMarketCap api_id 30 for currencies[k]
// slot last : kBlockFeeRate api_id 6
//
// app_id 100 - 128 are for the debt screens (kDebtSlotBase)
Author
First-time contributor

@djuri As there are 28 new screens, I have added only this line.

@djuri As there are 28 new screens, I have added only this line.
@ -73,3 +72,4 @@
// agnostic block (after NwcBalance) so the screen's introduction
// didn't shift bitaxe / NWC slot indices. New screens added later
// should follow the same pattern.
"base slot") {
Author
First-time contributor

@djuri Please test the "didn't shift bitaxe / NWC slot indices" part with this fix.

@djuri Please test the "didn't shift bitaxe / NWC slot indices" part with this fix.
Merge branch 'main' into feat/usdebt_screen
Some checks failed
Docs site / build (pull_request) Has been cancelled
Host tests / host_tests (pull_request) Has been cancelled
Host tests / sanitize (pull_request) Has been cancelled
Host tests / coverage (pull_request) Has been cancelled
Lint / format (pull_request) Has been cancelled
Lint / tidy (pull_request) Has been cancelled
61df9dc439
Owner

Review — US Debt screen over the v2 WS metric channel

Verdict: Request changes. This is a well-structured feature — the rate-frame-ticked-locally model is the right design, the slot/catalog integration is clean, and host-test coverage is solid. The wire protocol aligns 100% with the new Go metric feed. But there are two must-fixes before merge, and the debt socket's connection target is wired wrong — confirmed both by static analysis and empirically on hardware (Rev B).

Empirical verification (Rev B @ staging, dataSource=3ws-staging.btclock.dev)

I built this branch, flashed a Rev B, pointed it at staging, and read the live panel texts via GET /api/status (data[] = the current screen's panels), jumping straight to the debt screen with POST /api/show/screen {"s":100}:

Build data[] on screen 100 Result
A — branch as-is ['US/DEBT','','','','','',''] label only, all value cells blank
B — 2-line fix (below) ['US/DEBT','$','3','9.','2','6','T'] $39.26T, fully rendered from staging

In both runs the control price screen showed ['BTC/USD','$','6','2','9','3','7'] (≈ $62,937) — so the staging core feed was alive the whole time; only debt was silent in A. That isolates the failure precisely to the debt-metric routing.


🔴 Blockers

1. The debt-metric socket can never reach the server that serves the feed

main/sources/sources.cpp

The metric feed exists only on ws-staging.btclock.dev today (GET https://ws.btclock.dev/api/v2/metrics404). But the debt subscription is hardwired to production via two paths:

  • shares_debt_socket = (data_source == 0) (line ~199): with a custom endpoint (dataSource=3), the core socket that is connected to staging is handed an empty metric_keys list and never subscribes to debt:US.
  • The dedicated debt socket (line ~215, data_source != 0) is built with BuildBtclockSourceUri(0, "", false) = wss://ws.btclock.dev/api/v2/ws — hardcoded production, ignoring ceEndpoint.

So there is no settings/runtime path to point the debt socket at the configured custom endpoint. This is broader than the staging period: even after GA, a user running dataSource=3 against a self-hosted btclock endpoint that serves metrics would still not get debt. Hardware-verified fix (2 lines):

// line ~199 — 0/2/3 already hold a btclock-v2 core socket; ride it for debt:
const bool shares_debt_socket = data_source != 1;
// line ~215 — only mempool+kraken (ds=1) needs a separate debt socket:
if (data_source == 1) {

Optional refinement: have that data_source == 1 socket honor ceEndpoint instead of hardcoding production.

2. Host tests fail on CI — ceEndpoint default mismatch

test_host/test_settings_api.cpp asserts the ceEndpoint GET default is "ws.btclock.dev", but components/settings/include/settings/schema.hpp still declares kCeEndpoint's default as "ws-staging.btclock.dev" (and docs/SETTINGS.md agrees with the schema). GET /api/settings emits the schema default, so the assertion fails deterministically → btclock_host_tests exits non-zero → CI red. Looks like a rebase leftover (test updated, schema/doc not). Pick one source of truth.


🟢 Wire-contract alignment — fully verified

A live probe against wss://ws-staging.btclock.dev/api/v2/ws replaying the firmware's exact subscribe frame, plus feeding the real server bytes through the vendored ArduinoJson:

  • subscribe frame = 45 bytes (≪ the 96-byte buffer) → server acks Subscribed to debt:US
  • rate-frame base/rate/ref/dp encode as float64 / float64 / int64 / intis<double>() / is<double>() / is<int64_t>() / is<uint8_t>() all accept it.

Because the Go encoder statically types base/rate as float64, msgpack always emits 0xcb even for whole values, so the is<double>() guards are safe against integer-encoding drift by construction. No legitimate frame is ever dropped. The protocol side is correct; only the endpoint wiring was wrong.


🟠 Medium

  • BTC mode is wrong for small countriesmain/screens/panel_texts.cpp (FormatCompactDebt). The divisor table floors at 1e6 ('M'), so any debt-in-BTC below 1M renders as a misleading ₿0.0XM (e.g. Malta ≈ ₿0.11M), and the non-big-char path rounds *value to uint64 so a sub-1-BTC value collapses to 0. Add a sub-million tier and a host test for a small-country BTC value.
  • debtCurrency toggle bounces the WebSocketcomponents/webserver/control_server.cpp. It's in the on_screens_changed trigger, which posts kRebuildScreensSetSubscriptions → unconditional Stop()+Start() on the live socket. But debtCurrency only changes rendering; the subscription set is invariant. Route it through the live re-render / MarkDirty path instead of a TLS reconnect.
  • Always-on second WSS socket for every data_source != 0 user, even with zero debt screens enabled. Largely resolved by the Blocker-1 fix.
  • Rev A partition budgetmain/CMakeLists.txt links debt.cpp + the 29-entry catalog + ~170 lines of formatting unconditionally into the 4 MB image. Rev A sits at ~19 KiB free, so please measure the Rev A build before merge. Feature parity across all variants is the goal — if it regresses past the headroom, reclaim space by trimming the footprint (e.g. a leaner catalog representation, folding the duplicated formatting), not by excluding Rev A from the build.
  • Semantic merge conflict — of the conflicting files, only main/sources/sources.cpp truly conflicts on a 3-way merge. main moved currency-fetch into a deferred RefreshUpstreamCurrencies(); a naive resolution drops the USD/EUR price subscription (which debt-in-BTC depends on) after the first connect. Rebase manually and add a test that USD/EUR survive the post-connect prune.
  • DataSnapshot::Merge debt loop has no testcomponents/data_core/hub.cpp.

Low / nit (selection)

Per-tick ReadRenderPrefs() (~18 NVS reads) in the debt ShouldRender branch; the debt screen repaints every minSecPriceUpds while displayed (the counter ticks continuously — EPD wear); BuildDebtPanelTexts is built 3× per paint; dp is decoded but never consumed; dead starts_with("screen") branch in IsScreenVisibilityKey; ShouldRender/Render debt-key diverge only on the first frame (V8, N=8); untested edges in CurrentDebtValue (now<ref), the FormatCompactDebt unit-carry, and the 8-panel layout. Note: the "replace the US-only setting" comment is inaccurate — the prefs are purely additive, no migration involved.


Suggested order before merge

  1. Endpoint fix (2 lines, hardware-verified) — optionally honor ceEndpoint for the data_source == 1 socket too.
  2. Resolve the ceEndpoint default mismatch; get host tests green.
  3. Manual rebase onto main (semantic sources.cpp conflict) + a test that the debt USD/EUR subscription survives the prune.
  4. Measure the Rev A partition.
  5. BTC-mode sub-million tier; decouple debtCurrency from the WS bounce.

Reviewed by reading the full PR-branch files (not just the diff), an adversarially-verified multi-dimension pass, a live wire probe, and an on-device build/flash confirmation on Rev B.

# Review — US Debt screen over the v2 WS `metric` channel **Verdict: Request changes.** This is a well-structured feature — the rate-frame-ticked-locally model is the right design, the slot/catalog integration is clean, and host-test coverage is solid. The **wire protocol aligns 100% with the new Go `metric` feed**. But there are two must-fixes before merge, and the debt socket's connection target is wired wrong — confirmed both by static analysis and **empirically on hardware (Rev B)**. ## Empirical verification (Rev B @ staging, `dataSource=3` → `ws-staging.btclock.dev`) I built this branch, flashed a Rev B, pointed it at staging, and read the live panel texts via `GET /api/status` (`data[]` = the current screen's panels), jumping straight to the debt screen with `POST /api/show/screen {"s":100}`: | Build | `data[]` on screen 100 | Result | |---|---|---| | **A — branch as-is** | `['US/DEBT','','','','','','']` | label only, **all value cells blank** | | **B — 2-line fix (below)** | `['US/DEBT','$','3','9.','2','6','T']` | **$39.26T**, fully rendered from staging | In both runs the control price screen showed `['BTC/USD','$','6','2','9','3','7']` (≈ $62,937) — so the staging **core feed was alive the whole time**; only debt was silent in A. That isolates the failure precisely to the debt-metric routing. --- ## 🔴 Blockers ### 1. The debt-metric socket can never reach the server that serves the feed `main/sources/sources.cpp` The `metric` feed exists **only** on `ws-staging.btclock.dev` today (`GET https://ws.btclock.dev/api/v2/metrics` → `404`). But the debt subscription is hardwired to production via two paths: - `shares_debt_socket = (data_source == 0)` (line ~199): with a custom endpoint (`dataSource=3`), the core socket that *is* connected to staging is handed an **empty** `metric_keys` list and never subscribes to `debt:US`. - The dedicated debt socket (line ~215, `data_source != 0`) is built with `BuildBtclockSourceUri(0, "", false)` = `wss://ws.btclock.dev/api/v2/ws` — hardcoded production, **ignoring `ceEndpoint`**. So there is no settings/runtime path to point the debt socket at the configured custom endpoint. This is broader than the staging period: even after GA, a user running `dataSource=3` against a self-hosted btclock endpoint that serves metrics would still not get debt. **Hardware-verified fix (2 lines):** ```cpp // line ~199 — 0/2/3 already hold a btclock-v2 core socket; ride it for debt: const bool shares_debt_socket = data_source != 1; // line ~215 — only mempool+kraken (ds=1) needs a separate debt socket: if (data_source == 1) { ``` Optional refinement: have that `data_source == 1` socket honor `ceEndpoint` instead of hardcoding production. ### 2. Host tests fail on CI — `ceEndpoint` default mismatch `test_host/test_settings_api.cpp` asserts the `ceEndpoint` GET default is `"ws.btclock.dev"`, but `components/settings/include/settings/schema.hpp` still declares `kCeEndpoint`'s default as `"ws-staging.btclock.dev"` (and `docs/SETTINGS.md` agrees with the schema). `GET /api/settings` emits the schema default, so the assertion fails deterministically → `btclock_host_tests` exits non-zero → CI red. Looks like a rebase leftover (test updated, schema/doc not). Pick one source of truth. --- ## 🟢 Wire-contract alignment — fully verified A live probe against `wss://ws-staging.btclock.dev/api/v2/ws` replaying the firmware's exact subscribe frame, plus feeding the real server bytes through the vendored ArduinoJson: - subscribe frame = 45 bytes (≪ the 96-byte buffer) → server acks `Subscribed to debt:US` - rate-frame `base`/`rate`/`ref`/`dp` encode as **float64 / float64 / int64 / int** → `is<double>()` / `is<double>()` / `is<int64_t>()` / `is<uint8_t>()` all accept it. Because the Go encoder statically types `base`/`rate` as `float64`, msgpack always emits `0xcb` even for whole values, so the `is<double>()` guards are safe against integer-encoding drift by construction. No legitimate frame is ever dropped. The protocol side is correct; only the endpoint wiring was wrong. --- ## 🟠 Medium - **BTC mode is wrong for small countries** — `main/screens/panel_texts.cpp` (`FormatCompactDebt`). The divisor table floors at `1e6` ('M'), so any debt-in-BTC below 1M renders as a misleading `₿0.0XM` (e.g. Malta ≈ `₿0.11M`), and the non-big-char path rounds `*value` to `uint64` so a sub-1-BTC value collapses to `0`. Add a sub-million tier and a host test for a small-country BTC value. - **`debtCurrency` toggle bounces the WebSocket** — `components/webserver/control_server.cpp`. It's in the `on_screens_changed` trigger, which posts `kRebuildScreens` → `SetSubscriptions` → unconditional `Stop()`+`Start()` on the live socket. But `debtCurrency` only changes rendering; the subscription set is invariant. Route it through the live re-render / `MarkDirty` path instead of a TLS reconnect. - **Always-on second WSS socket** for every `data_source != 0` user, even with zero debt screens enabled. Largely resolved by the Blocker-1 fix. - **Rev A partition budget** — `main/CMakeLists.txt` links `debt.cpp` + the 29-entry catalog + ~170 lines of formatting unconditionally into the 4 MB image. Rev A sits at ~19 KiB free, so please measure the Rev A build before merge. Feature parity across all variants is the goal — if it regresses past the headroom, reclaim space by trimming the footprint (e.g. a leaner catalog representation, folding the duplicated formatting), **not** by excluding Rev A from the build. - **Semantic merge conflict** — of the conflicting files, only `main/sources/sources.cpp` truly conflicts on a 3-way merge. `main` moved currency-fetch into a deferred `RefreshUpstreamCurrencies()`; a naive resolution drops the USD/EUR price subscription (which debt-in-BTC depends on) after the first connect. Rebase manually and add a test that USD/EUR survive the post-connect prune. - **`DataSnapshot::Merge` debt loop has no test** — `components/data_core/hub.cpp`. ## ⚪ Low / nit (selection) Per-tick `ReadRenderPrefs()` (~18 NVS reads) in the debt `ShouldRender` branch; the debt screen repaints every `minSecPriceUpd`s while displayed (the counter ticks continuously — EPD wear); `BuildDebtPanelTexts` is built 3× per paint; `dp` is decoded but never consumed; dead `starts_with("screen")` branch in `IsScreenVisibilityKey`; `ShouldRender`/`Render` debt-key diverge only on the first frame (V8, N=8); untested edges in `CurrentDebtValue` (now<ref), the `FormatCompactDebt` unit-carry, and the 8-panel layout. Note: the "replace the US-only setting" comment is inaccurate — the prefs are purely additive, no migration involved. --- ## Suggested order before merge 1. Endpoint fix (2 lines, hardware-verified) — optionally honor `ceEndpoint` for the `data_source == 1` socket too. 2. Resolve the `ceEndpoint` default mismatch; get host tests green. 3. Manual rebase onto `main` (semantic `sources.cpp` conflict) + a test that the debt USD/EUR subscription survives the prune. 4. Measure the Rev A partition. 5. BTC-mode sub-million tier; decouple `debtCurrency` from the WS bounce. *Reviewed by reading the full PR-branch files (not just the diff), an adversarially-verified multi-dimension pass, a live wire probe, and an on-device build/flash confirmation on Rev B.*
fix: various review feedback
Some checks are pending
Docs site / build (pull_request) Blocked by required conditions
Host tests / host_tests (pull_request) Blocked by required conditions
Host tests / sanitize (pull_request) Blocked by required conditions
Host tests / coverage (pull_request) Blocked by required conditions
Lint / format (pull_request) Blocked by required conditions
Lint / tidy (pull_request) Blocked by required conditions
9e3c4b96e7
Some checks are pending
Docs site / build (pull_request) Blocked by required conditions
Host tests / host_tests (pull_request) Blocked by required conditions
Host tests / sanitize (pull_request) Blocked by required conditions
Host tests / coverage (pull_request) Blocked by required conditions
Lint / format (pull_request) Blocked by required conditions
Lint / tidy (pull_request) Blocked by required conditions
This pull request has changes conflicting with the target branch.
  • main/sources/sources.cpp
Some workflows are waiting to be reviewed.
View command line instructions

Manual merge helper

Use this merge commit message when completing the merge manually.

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u feat/usdebt_screen:PMK-feat/usdebt_screen
git switch PMK-feat/usdebt_screen

Merge

Merge the changes and update on Forgejo.

Warning: The "Autodetect manual merge" setting is not enabled for this repository, you will have to mark this pull request as manually merged afterwards.

git switch main
git merge --no-ff PMK-feat/usdebt_screen
git switch PMK-feat/usdebt_screen
git rebase main
git switch main
git merge --ff-only PMK-feat/usdebt_screen
git switch PMK-feat/usdebt_screen
git rebase main
git switch main
git merge --no-ff PMK-feat/usdebt_screen
git switch main
git merge --squash PMK-feat/usdebt_screen
git switch main
git merge --ff-only PMK-feat/usdebt_screen
git switch main
git merge PMK-feat/usdebt_screen
git push origin main
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
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
btclock/btclock_v4!2
No description provided.