feat(core-settings): expert-mode window with Moonbot's Settings dialog, live over the wire - #424
Merged
Merged
Conversation
The header gear gets a second face. An application-wide "expert mode" switch, off by default, makes it open a singleton tool window reproducing Moonbot's own Settings dialog — ten tabs in Moonbot's order — instead of the compact popover. This is the frame only: the pages are not drawn yet. Both faces send through one function. `Shell::commit_core_draft`'s send moves to `core_settings::draft::send_core_config`, which keeps the leverage clamp, the seeded-target guard, the section mask and the client-side blacklist-delta half in one place; the contract test that pinned that guard to `commit_core_draft` now pins it there and forbids the new window from calling `edit_core_config` itself. The window answers by explaining where the popup answers by closing, because a window that vanishes on its own reads as a save: `PageState` names why OK is dark — no core, Auto Overview, the active core moved, the page not arrived, a different MoonBot process on the connection, or the store's own `Stale`. A refused send keeps the window up with a banner, and the popup no longer closes on one either. Two fixes to the shared send fall out of that: it reports failure instead of returning `true` after `edit_core_config` errored, and it no longer rewrites an untouched `fix_lev` of 0 — the wire's "no fixed multiplier chosen" — to 1 on every OK. Tabs record how far each page's values reach: Основные and Автостарт are both projected and writable today, five more cross the wire but need `CoreConfig` and `FieldMask::RENDERED_SECTIONS` widened before they can be drawn, and Логин, Помощь с настройкой and PRO have nothing in safe-share at all. Every page opens regardless — hiding one would renumber the rest for a trader who reaches for a tab by position.
…ontrol Every tab of the expert window now reproduces Moonbot's own dialog in full, with each control the trader sees there drawn here — disabled where the terminal's projection carries no value for it. A missing row reads as a bug; a dead one reads as a limit. New pages: Interface, Hotkeys (read-only, decoding the core's real bindings across six inner tabs) and Special (four collapsible sections). Help and PRO are gone from the strip: both are Moonbot actions rather than values. Layout follows that dialog rather than approximating it. The window's root font is the UI face, not the monospaced one, so it stops disagreeing with the text inside it line by line; a caption is sized to its own text, so a sentence with boxes in it keeps them inside the sentence instead of at the row's edge; and the watchdog tracks are short and start halfway across, as they do there. Behaviour fixed while porting: - OK writes only the sections this window DRAWS. With the popup's wider mask it wrote a draft seeded when the window opened back over Leverage and the Signals alerts, silently undoing whatever had changed in them meanwhile. send_core_config takes the mask from its caller for that reason. - sync_from_core no longer binds the core before the state resolves. The binding was undone again by every state without a page, so the pair of writes reported a change on every backend notification — a repaint per tick, plus a scan of the configured servers, in the states that last longest. - The AutoStart report counters gate the repaint too, so a Reset shows its new number instead of waiting for an unrelated frame. - parse_num refuses a non-finite value. Rust parses "nan" and "inf", and these fields are thresholds: a NaN one compares false to everything, which turns a watchdog off while its checkbox still reads as on. - Closing a Special section blurs, as switching a tab already did: a disabled field takes focus on a click, and focus left on a control nothing draws takes the window's dispatch path with it. - The gear pressed twice on its own group keeps the staged page. - A refused Reset says so in the log, as a refused OK already did.
The expert window's Interface tab was drawn in full and disabled. Half of what Moonbot puts on that page does cross the safe-share wire, so it is now seeded from the core and sent on OK: 22 switches, its two alert-sound pickers with their levels, the order-book zone transparencies, the two line widths and the pending-order spread. A new `InterfaceSettings` block carries them — 28 fields spread across the wire's `trading`, `visual`, `signals` and `ui` sections, because a projected area is a PAGE rather than a wire section: what a surface may write is what it draws, and Moonbot's Interface page draws across all four. That principle also changed how OK builds its mask. It used to be a constant naming everything the window could reach, so an OK pressed on General wrote this window's copy of the Interface block, frozen when it opened, back over whatever the Moonbot user had changed there meanwhile. The mask is now folded from the tabs whose controls were actually touched (`ExpertTab::add_sections`), and an OK that touched nothing sendable closes without a write. Three of Moonbot's rows stay disabled on purpose, and the module docs say why: `trading.pending_buy_price` is not the drawing flag its caption suggests — the wire documents it as using the pending-buy price for SELL calculations, so a cosmetic-looking checkbox would have changed sell maths on a live bot; `trading.use_lev_for_take` already belongs to the manual block, and one wire field projected into two areas leaves the second stale after a write; `visual.manual_charts_full_screen` sits behind its section's tail gate, so an older core reads it back as false however it was written. Four more rows are disabled because which wire field backs them does not follow from the protocol's own documentation, and a mirrored control wired to the wrong field is worse than one that plainly does nothing. Also: - `InterfaceSettings` compares its two `f64` spreads with `total_cmp`, as `ManualSettings` already did: a core holding a non-finite one must still compare equal to itself, or every edit on it burns its attempts and gives up. - The spinners declare no range. The component clamps what it DISPLAYS, so a range there showed one number while OK sent another, and an arrow press then stepped from the displayed one and discarded the core's value. - The spread boxes print a value that reads back as itself, and refuse what falls outside a sanity range instead of quietly rewriting it. - `set_exclude_blacklisted_delta`, the client-side half of a core-side filter, now travels only with the section that owns its checkbox. MoonUI's disabled slider still emits on a thumb drag — its bar guards its handlers, its thumb does not. Logged in docs-internal/FORK_BUGS.md.
The densest page of the expert window was drawn in full and disabled. Almost all of it crosses the safe-share wire, so it is now seeded from the core and sent on OK: 18 switches, the five keyword lists, the word-distance, dip and message-price offsets, the auto-cancel timer and the message word cap. A new `AutoBuySettings` block carries them — 31 fields out of the `signals` section, its `signal_config` sub-record and the one `trading` field Moonbot files under this page. An area is a PAGE, not a wire section, so it does not overlap `SignalsSettings`: those six alert-sound fields live on the Interface page, where Moonbot draws them, and one wire field belongs to one area or a write from either surface would put the other's frozen copy back. Rows that stay disabled, and why, are named in the page's module doc. The TradingView webhook and "не покупать пересланное" have no wire field at all. The three "search mode" buttons turned out not to be a mode: `look_full_link_*` is an additive parse option and `advanced_filter*` a separate feature, and the wire's own default sets both, so the page shows each flag as itself, draws the third button dead because nothing says what it means, and accepts no click — an exclusive write would clear a flag the trader never touched. The two group titles (`monitor_clipboard`, `do_monitoring`) are unclaimed for the same reason: this window draws them as frame captions rather than switches. Also: `widgets::radio` is read-only again and `widgets::stepper` is gone, both following what the pages actually need.
… dialog The protocol reads as if the three "search mode" buttons were two independent flags: `SignalsSection::default()` sets `look_full_link_*` and `advanced_filter*` together, which no one-of-three control can express, so the group was made read-only. Moonbot's own dialog settles it the other way — it draws radio buttons with exactly one selected, so that default is a factory value its OK normalises rather than a state its user can reach. So the mode stages again, and picking one writes the whole choice. A click on the option ALREADY selected now stages nothing, which is what makes that safe: without it, opening this window on a factory-default core and clicking the mode it already shows would clear the other flag. The same dialog restores `signals.monitor_clipboard` to "захватывать буфер". It had been unbound because the wire calls it "enable clipboard monitoring", which reads like a title for the group — but that dialog puts no switch in either frame caption, and this is the only clipboard control left once auto-buy, lowercase and the mode pair have their fields. `signals.do_monitoring` stays unclaimed: it is the master toggle for the whole signal pipeline and has no control on this page.
The channel box shows the core's real channels and the four rules over them are staged and sent on OK. A new `TelegramSettings` block carries six fields out of `signals` plus the one `trading` flag Moonbot files under this tab; it does not overlap `AutoBuySettings`, which owns how a message is parsed, or `SignalsSettings`, which owns the alert sounds. This is the window's first list a user can change, which brought two things with it. A pick, held by the WINDOW because a page is rebuilt every render, and cleared everywhere the page it points into is dropped: on a re-seed, on a tab switch, on a blocked state, on a rebind. Removal then matches by the picked row AND by name — the core can publish a new list between the frame that drew the pick and the click that acts on it, and an index alone would still be in range while naming a different channel. And a text box that stages nothing. `editors::scratch_input_state` builds a control with no subscription, for a value on its way somewhere else rather than into the draft: routed through the normal path, one keystroke in the "add a channel" box marked the page edited, froze it against the core, and put the whole Telegram section on the wire for a name still being typed. Adding a channel strips the "@" Moonbot draws as a caption beside the box, refuses a duplicate, fills an empty primary channel rather than appending to a list the core only reads in multi-channel mode, and empties the box once the name has landed. A removal or an add that changes nothing does not mark the page edited. Moonbot's built-in Telegram client — its switch, proxy, status and log-out — stays dead, as does "отправлять статистику на сервер": the safe-share subset carries no part of either.
…reaches A new `SpecialSettings` block carries 28 fields out of `trading` and its `send_shots_config` sub-record, and the page's engine switches, its logging and its screenshot rules are seeded from the core and sent on OK: 14 switches, 8 boxes and 5 sliders. What stays disabled, and why, is stated on the page. The Remote block's own identity — the bot token, its PIN, the UDP password, the control VDS address — is outside the safe-share subset, so what is live inside that section is only the screenshot rules and the multi-command switch. The iceberg pair belongs to `GeneralSettings`, which the compact popup edits. A few rows have no wire field that means what their caption says: `trading.free_position_check`, the nearest neighbour of "Не проверять лимиты позиции", is documented as CLOSING orphaned positions and travels on the compact ClientSettings route besides. And `trading.orders_control`, behind several more rows, is on the wire but not projected yet. Three captions word their field differently from the protocol's own doc, and the projection records each: "Мультистроковые команды" against a batch-order flag, "или профит за час" against a session profit, "Ось времени, %" against seconds of history. Each binding rests on there being no other candidate in its sub-record. The iceberg slider is bounded 0..1 because the wire calls that field a fraction of the order, and its staged value is rounded to the track's own step: 0.01 has no exact f32, so the raw widening put 0.07000000029802322 on the wire for a thumb dropped on 0.07. The numeric boxes refuse a negative count or one outside i32 rather than saturating through `as`, which turned a typo into i32::MAX — and refuse nothing else, since the wire's own defaults include a price scale of zero. "No trades on markets" is shown but not editable: the wire holds that list one ticker per line and this page draws a single-line box, which neither renders the second line nor could keep it.
Seven more rows of Moonbot's "Специальные" page are live: Liquidation Control and Ignore Replacing Bug in the engine block, Turn Off Protection in System, and in the hang watchdog the worker-bot switch, Report to Telegram, AutoSell and the list of balances the watcher leaves alone. Four fields of `trading.orders_control` stay out, and each has its own reason. `sign_orders` is marked on the wire as a mirror of `ClientSettingsCommand::sign_orders`, so it travels on the compact channel too and writing it here would set two routes fighting over one field. `min_price` and `max_time` are this core's own watchdog thresholds, which no row of the worker-bot block sets — the snapshot carries no follower list, so the columns Moonbot shows in its table are a watcher's view of other bots. And `h_pos_control` has no caption of its own: the single switch above says "следить за ОРДЕРАМИ", which is `orders_control.active`, and binding one checkbox to two flags would turn a feature on and off that nobody named. What that costs is stated on the page rather than hidden: Report to Telegram and AutoSell act only while hanging-position detection is on in the core. The test asserts all four survive a write untouched. `ignore_protection` is a bypass LEVEL under a checkbox. Ticking it supplies a level only when the core holds none, so a level already set survives; a negative reads as protection on, the wire stating a meaning for zero and for a level but not for less than zero. The balances box edits a one-line comma-separated list, which is what Moonbot's own dialog and documentation show — the wire names no separator for this field. It strips a stray carriage return on the way in, because moonui's single-line paste drops a newline but keeps a lone CR, and it refuses to edit at all if the core somehow sends a value this control cannot render whole.
…e reasons The label was a link in Moonbot and is nothing here, so the four copies of it go, along with the locale key they were the only readers of. Two comments in this window stated a reason that does not hold, and a comment that misnames the wire is worse than none — the next reader takes it as established. The General page claimed the stop-loss pair is held back by the wire's "Mirrors `ClientSettingsCommand::…`" note. Five fields that page already writes carry the same note: `trailing_stop`, `g_take_profit`, `vol_drop_level`, `coins_black_list_text`, `use_coins_black_list`. The note says the compact channel carries the same field, not that this one may not. The real reason is the plain one that holds for every other dead row: the field is outside `GeneralSettings`, so there is nothing to seed it from. The same page called Copy/Paste of the settings something that "does not cross the wire". moonproto exports the very format those buttons use — `shared_config::to_mbsc_string` / `from_mbsc_string` and the `.mbshare` byte form — and this window holds the whole snapshot. They are unimplemented, not impossible, and now say so. Also from the audit: the Interface page's doc lumped three rows in with "could not be established", when each has a concrete reason recorded in `moon_core`; the Autostart page's layout comment still described a help link that is gone; and the ten manual-strategy checkboxes shared one GPUI `ElementId`, which makes ten controls into one. Harmless while all ten are disabled and stage nothing, which is why it went unseen — and a real collision the day that block becomes writable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e tab Seven rows of Moonbot's "Основные" page and the whole "Orders Controls" tab of its Hotkeys page are live. On General: the trailing addition, the partial-fill sell threshold, the buy auto-cancel, the cancel-after-Sell switch, the fresh-coin hold, the trades-into-deltas switch, and the startup analysis above the two columns. `auto_cancel_buy_order` is a COMPOSITE scale and the one number on that page that is not what it reads as: the wire documents values below 30 as seconds and 30 and above as `value - 29` minutes. The page prints the unit and sends the value untouched. Zero is drawn as zero seconds, not as "off" — the wire states no meaning for it, and naming one is how a row comes to tell a trader the opposite of what the core will do. `deltas_by_trades` has a client-side half, like the blacklist-delta filter beside it: moonproto applies its own copy to the analytics this terminal retains, so the row drives both through a new `CoreCmd::SetDeltasByTrades`. The seven are their own projection area, `OrderRulesSettings`, although they share a Moonbot page with the exits. The compact gear popup draws the exits and the blacklist and NOT these, and a mask names what a surface DREW: carrying them in the popup's mask would let its OK stamp all seven back from a frozen draft over whatever Moonbot changed meanwhile. A test asserts it does not. On Hotkeys, sixteen selectors and one checkbox become `GestureSettings`. Sixteen fields of `trading.multi_orders` plus `trading.pending_order_set_click` beside it — the wire carries no pending-LONG inside that sub-record, so Moonbot's own pair of rows straddles two records. Of the eight left out, `join_sell_kind` mirrors the compact channel and would set two routes fighting over one field; six belong to Moonbot's chart rather than to its gestures; and `ver` is the wire's version byte. The ordinals are not guessed. moonproto's own defaults annotate `buy_set_click: 1` as Dbl_Click and `sell_move_click: 2` as CTRL_Click, which is `MouseGestureBinding::ALL` at 1 and 2; `TReplaceMultiKind` (Vars.pas:37) runs None=0 through LastMoved=7, exactly as `MoveKind::ALL` is ordered. A new test pins every position of both lists: their docs call them a display order, and a reorder would silently rewrite the stored gestures of every core. "Одни хоткеи для Long и Short" mirrors the long gestures onto the short ones, as Moonbot's dialog and this terminal's own Hotkeys tab both do. The two sources for reading that flag disagree — Moonbot's caption and the shipped port say Long/Short, moonproto's one-line gloss says primary/secondary — and the disagreement is recorded in the code rather than resolved silently. The rule lives on `GestureSettings` with the reader that resolves the short column to what actually fires, beside where `config::hotkeys` keeps the same pair for the terminal's own copy. The Hotkeys page is `Projected` now, which retires `TabSource::Wire`: no page is on the wire but unprojected any more. Its five read-only sub-tabs and the Controls grid say so themselves, since the window's banner is per page and this page is only partly live. Ancillary: `MouseGestureBinding::menu_label` and `MoveKind::locale_key` are the one place each label is spelled, so the two surfaces drawing these lists cannot drift; the gesture list is built once per process and borrowed, and a disabled selector builds no menu items at all, where the first revision rebuilt ~224 of them per frame; the hand-written `PartialEq` impls destructure `Self`, so a field added later fails to compile rather than silently dropping out of equality; and the trailing addition rounds through `util::fmt::round_to`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e whole snapshot `SharedConfigSequence` asked the wrong question of the core's echo. The confirmation compared the WHOLE projection, so anything a trader moved in Moonbot's own dialogs while a packet was in flight made the echo differ. That matched neither the "confirmed" branch nor the "rejected" one, so the edit resolved as nothing, the whole snapshot was sent again, and enough of those in a row dropped a write that had LANDED as `GaveUp`. `rejection_within_mask` already knew the difference — it restricts itself to the areas the edit named. The confirmation now goes through it, and so does `edit_satisfied`, which had the same defect from the other side: an OK that changed nothing still sent the whole snapshot whenever any unrelated area had drifted since the surface seeded. Three questions, one comparison. Both regressions are covered by tests written before the fix and confirmed to fail without it. Narrowing `edit_satisfied` creates a new way to fail silently: a mask bit with no comparison arm would no longer merely lose a rejection, it would make every edit naming that bit drop from the queue without ever being sent. So `rejection_within_mask` and `apply_core_config` both destructure `FieldMask` — a bit added later cannot compile without being named in each. That is the half worth having; leaving a named bit unused is only a warning. It also made "this entry is already satisfied" common where it had been nearly unreachable, which exposed a second bug. There is ONE edit row per core and `Confirmed` clears it outright, so a satisfied entry drained in the same pass as a give-up erased the give-up — a trader would read a write that never applied as saved. The suppression that already existed for a rejection now covers both terminal verdicts. The cost is stated where it is paid: that entry's own success goes unannounced. `AutoStartSettings` and `BtcBlinkSettings` were the last two projection areas still deriving `PartialEq` over wire `f64`s, which matters more now that one comparison answers all three questions: a non-finite value makes an area never equal itself, so every OK naming it burns the budget. Both are hand-written against `total_cmp` and destructure `Self`, like the five areas that already did. `parse_num` canonicalises `-0.0` in the same spirit, at the one place every typed number enters rather than in each comparison downstream. Ancillary, from the same review: the three alert-sound pickers in the expert window were gated on the alert's own switch while the level beside them was not, and the compact popup gates neither — the switch turns the alert on and off, not the ability to choose which sound it will use. With the last one ungated the `enabled` parameter had no caller left and is gone. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
guyverino
added a commit
that referenced
this pull request
Sep 5, 2026
…g, live over the wire (#424) * feat(core-settings): expert-mode window with Moonbot's settings tabs The header gear gets a second face. An application-wide "expert mode" switch, off by default, makes it open a singleton tool window reproducing Moonbot's own Settings dialog — ten tabs in Moonbot's order — instead of the compact popover. This is the frame only: the pages are not drawn yet. Both faces send through one function. `Shell::commit_core_draft`'s send moves to `core_settings::draft::send_core_config`, which keeps the leverage clamp, the seeded-target guard, the section mask and the client-side blacklist-delta half in one place; the contract test that pinned that guard to `commit_core_draft` now pins it there and forbids the new window from calling `edit_core_config` itself. The window answers by explaining where the popup answers by closing, because a window that vanishes on its own reads as a save: `PageState` names why OK is dark — no core, Auto Overview, the active core moved, the page not arrived, a different MoonBot process on the connection, or the store's own `Stale`. A refused send keeps the window up with a banner, and the popup no longer closes on one either. Two fixes to the shared send fall out of that: it reports failure instead of returning `true` after `edit_core_config` errored, and it no longer rewrites an untouched `fix_lev` of 0 — the wire's "no fixed multiplier chosen" — to 1 on every OK. Tabs record how far each page's values reach: Основные and Автостарт are both projected and writable today, five more cross the wire but need `CoreConfig` and `FieldMask::RENDERED_SECTIONS` widened before they can be drawn, and Логин, Помощь с настройкой and PRO have nothing in safe-share at all. Every page opens regardless — hiding one would renumber the rest for a trader who reaches for a tab by position. * feat(core-settings): draw Moonbot's eight expert pages, control for control Every tab of the expert window now reproduces Moonbot's own dialog in full, with each control the trader sees there drawn here — disabled where the terminal's projection carries no value for it. A missing row reads as a bug; a dead one reads as a limit. New pages: Interface, Hotkeys (read-only, decoding the core's real bindings across six inner tabs) and Special (four collapsible sections). Help and PRO are gone from the strip: both are Moonbot actions rather than values. Layout follows that dialog rather than approximating it. The window's root font is the UI face, not the monospaced one, so it stops disagreeing with the text inside it line by line; a caption is sized to its own text, so a sentence with boxes in it keeps them inside the sentence instead of at the row's edge; and the watchdog tracks are short and start halfway across, as they do there. Behaviour fixed while porting: - OK writes only the sections this window DRAWS. With the popup's wider mask it wrote a draft seeded when the window opened back over Leverage and the Signals alerts, silently undoing whatever had changed in them meanwhile. send_core_config takes the mask from its caller for that reason. - sync_from_core no longer binds the core before the state resolves. The binding was undone again by every state without a page, so the pair of writes reported a change on every backend notification — a repaint per tick, plus a scan of the configured servers, in the states that last longest. - The AutoStart report counters gate the repaint too, so a Reset shows its new number instead of waiting for an unrelated frame. - parse_num refuses a non-finite value. Rust parses "nan" and "inf", and these fields are thresholds: a NaN one compares false to everything, which turns a watchdog off while its checkbox still reads as on. - Closing a Special section blurs, as switching a tab already did: a disabled field takes focus on a click, and focus left on a control nothing draws takes the window's dispatch path with it. - The gear pressed twice on its own group keeps the staged page. - A refused Reset says so in the log, as a refused OK already did. * feat(core-settings): make Moonbot's Interface page live over the wire The expert window's Interface tab was drawn in full and disabled. Half of what Moonbot puts on that page does cross the safe-share wire, so it is now seeded from the core and sent on OK: 22 switches, its two alert-sound pickers with their levels, the order-book zone transparencies, the two line widths and the pending-order spread. A new `InterfaceSettings` block carries them — 28 fields spread across the wire's `trading`, `visual`, `signals` and `ui` sections, because a projected area is a PAGE rather than a wire section: what a surface may write is what it draws, and Moonbot's Interface page draws across all four. That principle also changed how OK builds its mask. It used to be a constant naming everything the window could reach, so an OK pressed on General wrote this window's copy of the Interface block, frozen when it opened, back over whatever the Moonbot user had changed there meanwhile. The mask is now folded from the tabs whose controls were actually touched (`ExpertTab::add_sections`), and an OK that touched nothing sendable closes without a write. Three of Moonbot's rows stay disabled on purpose, and the module docs say why: `trading.pending_buy_price` is not the drawing flag its caption suggests — the wire documents it as using the pending-buy price for SELL calculations, so a cosmetic-looking checkbox would have changed sell maths on a live bot; `trading.use_lev_for_take` already belongs to the manual block, and one wire field projected into two areas leaves the second stale after a write; `visual.manual_charts_full_screen` sits behind its section's tail gate, so an older core reads it back as false however it was written. Four more rows are disabled because which wire field backs them does not follow from the protocol's own documentation, and a mirrored control wired to the wrong field is worse than one that plainly does nothing. Also: - `InterfaceSettings` compares its two `f64` spreads with `total_cmp`, as `ManualSettings` already did: a core holding a non-finite one must still compare equal to itself, or every edit on it burns its attempts and gives up. - The spinners declare no range. The component clamps what it DISPLAYS, so a range there showed one number while OK sent another, and an arrow press then stepped from the displayed one and discarded the core's value. - The spread boxes print a value that reads back as itself, and refuse what falls outside a sanity range instead of quietly rewriting it. - `set_exclude_blacklisted_delta`, the client-side half of a core-side filter, now travels only with the section that owns its checkbox. MoonUI's disabled slider still emits on a thumb drag — its bar guards its handlers, its thumb does not. Logged in docs-internal/FORK_BUGS.md. * feat(core-settings): make Moonbot's AutoBuy page live over the wire The densest page of the expert window was drawn in full and disabled. Almost all of it crosses the safe-share wire, so it is now seeded from the core and sent on OK: 18 switches, the five keyword lists, the word-distance, dip and message-price offsets, the auto-cancel timer and the message word cap. A new `AutoBuySettings` block carries them — 31 fields out of the `signals` section, its `signal_config` sub-record and the one `trading` field Moonbot files under this page. An area is a PAGE, not a wire section, so it does not overlap `SignalsSettings`: those six alert-sound fields live on the Interface page, where Moonbot draws them, and one wire field belongs to one area or a write from either surface would put the other's frozen copy back. Rows that stay disabled, and why, are named in the page's module doc. The TradingView webhook and "не покупать пересланное" have no wire field at all. The three "search mode" buttons turned out not to be a mode: `look_full_link_*` is an additive parse option and `advanced_filter*` a separate feature, and the wire's own default sets both, so the page shows each flag as itself, draws the third button dead because nothing says what it means, and accepts no click — an exclusive write would clear a flag the trader never touched. The two group titles (`monitor_clipboard`, `do_monitoring`) are unclaimed for the same reason: this window draws them as frame captions rather than switches. Also: `widgets::radio` is read-only again and `widgets::stepper` is gone, both following what the pages actually need. * fix(core-settings): restore the AutoBuy search mode against Moonbot's dialog The protocol reads as if the three "search mode" buttons were two independent flags: `SignalsSection::default()` sets `look_full_link_*` and `advanced_filter*` together, which no one-of-three control can express, so the group was made read-only. Moonbot's own dialog settles it the other way — it draws radio buttons with exactly one selected, so that default is a factory value its OK normalises rather than a state its user can reach. So the mode stages again, and picking one writes the whole choice. A click on the option ALREADY selected now stages nothing, which is what makes that safe: without it, opening this window on a factory-default core and clicking the mode it already shows would clear the other flag. The same dialog restores `signals.monitor_clipboard` to "захватывать буфер". It had been unbound because the wire calls it "enable clipboard monitoring", which reads like a title for the group — but that dialog puts no switch in either frame caption, and this is the only clipboard control left once auto-buy, lowercase and the mode pair have their fields. `signals.do_monitoring` stays unclaimed: it is the master toggle for the whole signal pipeline and has no control on this page. * feat(core-settings): make Moonbot's Telegram page live over the wire The channel box shows the core's real channels and the four rules over them are staged and sent on OK. A new `TelegramSettings` block carries six fields out of `signals` plus the one `trading` flag Moonbot files under this tab; it does not overlap `AutoBuySettings`, which owns how a message is parsed, or `SignalsSettings`, which owns the alert sounds. This is the window's first list a user can change, which brought two things with it. A pick, held by the WINDOW because a page is rebuilt every render, and cleared everywhere the page it points into is dropped: on a re-seed, on a tab switch, on a blocked state, on a rebind. Removal then matches by the picked row AND by name — the core can publish a new list between the frame that drew the pick and the click that acts on it, and an index alone would still be in range while naming a different channel. And a text box that stages nothing. `editors::scratch_input_state` builds a control with no subscription, for a value on its way somewhere else rather than into the draft: routed through the normal path, one keystroke in the "add a channel" box marked the page edited, froze it against the core, and put the whole Telegram section on the wire for a name still being typed. Adding a channel strips the "@" Moonbot draws as a caption beside the box, refuses a duplicate, fills an empty primary channel rather than appending to a list the core only reads in multi-channel mode, and empties the box once the name has landed. A removal or an add that changes nothing does not mark the page edited. Moonbot's built-in Telegram client — its switch, proxy, status and log-out — stays dead, as does "отправлять статистику на сервер": the safe-share subset carries no part of either. * feat(core-settings): make Moonbot's Special page live where the wire reaches A new `SpecialSettings` block carries 28 fields out of `trading` and its `send_shots_config` sub-record, and the page's engine switches, its logging and its screenshot rules are seeded from the core and sent on OK: 14 switches, 8 boxes and 5 sliders. What stays disabled, and why, is stated on the page. The Remote block's own identity — the bot token, its PIN, the UDP password, the control VDS address — is outside the safe-share subset, so what is live inside that section is only the screenshot rules and the multi-command switch. The iceberg pair belongs to `GeneralSettings`, which the compact popup edits. A few rows have no wire field that means what their caption says: `trading.free_position_check`, the nearest neighbour of "Не проверять лимиты позиции", is documented as CLOSING orphaned positions and travels on the compact ClientSettings route besides. And `trading.orders_control`, behind several more rows, is on the wire but not projected yet. Three captions word their field differently from the protocol's own doc, and the projection records each: "Мультистроковые команды" against a batch-order flag, "или профит за час" against a session profit, "Ось времени, %" against seconds of history. Each binding rests on there being no other candidate in its sub-record. The iceberg slider is bounded 0..1 because the wire calls that field a fraction of the order, and its staged value is rounded to the track's own step: 0.01 has no exact f32, so the raw widening put 0.07000000029802322 on the wire for a thumb dropped on 0.07. The numeric boxes refuse a negative count or one outside i32 rather than saturating through `as`, which turned a typo into i32::MAX — and refuse nothing else, since the wire's own defaults include a price scale of zero. "No trades on markets" is shown but not editable: the wire holds that list one ticker per line and this page draws a single-line box, which neither renders the second line nor could keep it. * feat(core-settings): wire the orders_control block on the Special page Seven more rows of Moonbot's "Специальные" page are live: Liquidation Control and Ignore Replacing Bug in the engine block, Turn Off Protection in System, and in the hang watchdog the worker-bot switch, Report to Telegram, AutoSell and the list of balances the watcher leaves alone. Four fields of `trading.orders_control` stay out, and each has its own reason. `sign_orders` is marked on the wire as a mirror of `ClientSettingsCommand::sign_orders`, so it travels on the compact channel too and writing it here would set two routes fighting over one field. `min_price` and `max_time` are this core's own watchdog thresholds, which no row of the worker-bot block sets — the snapshot carries no follower list, so the columns Moonbot shows in its table are a watcher's view of other bots. And `h_pos_control` has no caption of its own: the single switch above says "следить за ОРДЕРАМИ", which is `orders_control.active`, and binding one checkbox to two flags would turn a feature on and off that nobody named. What that costs is stated on the page rather than hidden: Report to Telegram and AutoSell act only while hanging-position detection is on in the core. The test asserts all four survive a write untouched. `ignore_protection` is a bypass LEVEL under a checkbox. Ticking it supplies a level only when the core holds none, so a level already set survives; a negative reads as protection on, the wire stating a meaning for zero and for a level but not for less than zero. The balances box edits a one-line comma-separated list, which is what Moonbot's own dialog and documentation show — the wire names no separator for this field. It strips a stray carriage return on the way in, because moonui's single-line paste drops a newline but keeps a lone CR, and it refuses to edit at all if the core somehow sends a value this control cannot render whole. * fix(core-settings): drop the "Need help!" labels and correct two false reasons The label was a link in Moonbot and is nothing here, so the four copies of it go, along with the locale key they were the only readers of. Two comments in this window stated a reason that does not hold, and a comment that misnames the wire is worse than none — the next reader takes it as established. The General page claimed the stop-loss pair is held back by the wire's "Mirrors `ClientSettingsCommand::…`" note. Five fields that page already writes carry the same note: `trailing_stop`, `g_take_profit`, `vol_drop_level`, `coins_black_list_text`, `use_coins_black_list`. The note says the compact channel carries the same field, not that this one may not. The real reason is the plain one that holds for every other dead row: the field is outside `GeneralSettings`, so there is nothing to seed it from. The same page called Copy/Paste of the settings something that "does not cross the wire". moonproto exports the very format those buttons use — `shared_config::to_mbsc_string` / `from_mbsc_string` and the `.mbshare` byte form — and this window holds the whole snapshot. They are unimplemented, not impossible, and now say so. Also from the audit: the Interface page's doc lumped three rows in with "could not be established", when each has a concrete reason recorded in `moon_core`; the Autostart page's layout comment still described a help link that is gone; and the ten manual-strategy checkboxes shared one GPUI `ElementId`, which makes ten controls into one. Harmless while all ten are disabled and stage nothing, which is why it went unseen — and a real collision the day that block becomes writable. * feat(core-settings): live order rules on General and the whole gesture tab Seven rows of Moonbot's "Основные" page and the whole "Orders Controls" tab of its Hotkeys page are live. On General: the trailing addition, the partial-fill sell threshold, the buy auto-cancel, the cancel-after-Sell switch, the fresh-coin hold, the trades-into-deltas switch, and the startup analysis above the two columns. `auto_cancel_buy_order` is a COMPOSITE scale and the one number on that page that is not what it reads as: the wire documents values below 30 as seconds and 30 and above as `value - 29` minutes. The page prints the unit and sends the value untouched. Zero is drawn as zero seconds, not as "off" — the wire states no meaning for it, and naming one is how a row comes to tell a trader the opposite of what the core will do. `deltas_by_trades` has a client-side half, like the blacklist-delta filter beside it: moonproto applies its own copy to the analytics this terminal retains, so the row drives both through a new `CoreCmd::SetDeltasByTrades`. The seven are their own projection area, `OrderRulesSettings`, although they share a Moonbot page with the exits. The compact gear popup draws the exits and the blacklist and NOT these, and a mask names what a surface DREW: carrying them in the popup's mask would let its OK stamp all seven back from a frozen draft over whatever Moonbot changed meanwhile. A test asserts it does not. On Hotkeys, sixteen selectors and one checkbox become `GestureSettings`. Sixteen fields of `trading.multi_orders` plus `trading.pending_order_set_click` beside it — the wire carries no pending-LONG inside that sub-record, so Moonbot's own pair of rows straddles two records. Of the eight left out, `join_sell_kind` mirrors the compact channel and would set two routes fighting over one field; six belong to Moonbot's chart rather than to its gestures; and `ver` is the wire's version byte. The ordinals are not guessed. moonproto's own defaults annotate `buy_set_click: 1` as Dbl_Click and `sell_move_click: 2` as CTRL_Click, which is `MouseGestureBinding::ALL` at 1 and 2; `TReplaceMultiKind` (Vars.pas:37) runs None=0 through LastMoved=7, exactly as `MoveKind::ALL` is ordered. A new test pins every position of both lists: their docs call them a display order, and a reorder would silently rewrite the stored gestures of every core. "Одни хоткеи для Long и Short" mirrors the long gestures onto the short ones, as Moonbot's dialog and this terminal's own Hotkeys tab both do. The two sources for reading that flag disagree — Moonbot's caption and the shipped port say Long/Short, moonproto's one-line gloss says primary/secondary — and the disagreement is recorded in the code rather than resolved silently. The rule lives on `GestureSettings` with the reader that resolves the short column to what actually fires, beside where `config::hotkeys` keeps the same pair for the terminal's own copy. The Hotkeys page is `Projected` now, which retires `TabSource::Wire`: no page is on the wire but unprojected any more. Its five read-only sub-tabs and the Controls grid say so themselves, since the window's banner is per page and this page is only partly live. Ancillary: `MouseGestureBinding::menu_label` and `MoveKind::locale_key` are the one place each label is spelled, so the two surfaces drawing these lists cannot drift; the gesture list is built once per process and borrowed, and a disabled selector builds no menu items at all, where the first revision rebuilt ~224 of them per frame; the hand-written `PartialEq` impls destructure `Self`, so a field added later fails to compile rather than silently dropping out of equality; and the trailing addition rounds through `util::fmt::round_to`. * fix(core-settings): confirm a shared-config write by its mask, not the whole snapshot `SharedConfigSequence` asked the wrong question of the core's echo. The confirmation compared the WHOLE projection, so anything a trader moved in Moonbot's own dialogs while a packet was in flight made the echo differ. That matched neither the "confirmed" branch nor the "rejected" one, so the edit resolved as nothing, the whole snapshot was sent again, and enough of those in a row dropped a write that had LANDED as `GaveUp`. `rejection_within_mask` already knew the difference — it restricts itself to the areas the edit named. The confirmation now goes through it, and so does `edit_satisfied`, which had the same defect from the other side: an OK that changed nothing still sent the whole snapshot whenever any unrelated area had drifted since the surface seeded. Three questions, one comparison. Both regressions are covered by tests written before the fix and confirmed to fail without it. Narrowing `edit_satisfied` creates a new way to fail silently: a mask bit with no comparison arm would no longer merely lose a rejection, it would make every edit naming that bit drop from the queue without ever being sent. So `rejection_within_mask` and `apply_core_config` both destructure `FieldMask` — a bit added later cannot compile without being named in each. That is the half worth having; leaving a named bit unused is only a warning. It also made "this entry is already satisfied" common where it had been nearly unreachable, which exposed a second bug. There is ONE edit row per core and `Confirmed` clears it outright, so a satisfied entry drained in the same pass as a give-up erased the give-up — a trader would read a write that never applied as saved. The suppression that already existed for a rejection now covers both terminal verdicts. The cost is stated where it is paid: that entry's own success goes unannounced. `AutoStartSettings` and `BtcBlinkSettings` were the last two projection areas still deriving `PartialEq` over wire `f64`s, which matters more now that one comparison answers all three questions: a non-finite value makes an area never equal itself, so every OK naming it burns the budget. Both are hand-written against `total_cmp` and destructure `Self`, like the five areas that already did. `parse_num` canonicalises `-0.0` in the same spirit, at the one place every typed number enters rather than in each comparison downstream. Ancillary, from the same review: the three alert-sound pickers in the expert window were gated on the alert's own switch while the level beside them was not, and the compact popup gates neither — the switch turns the alert on and off, not the ability to choose which sound it will use. With the last one ungated the `enabled` parameter had no caller left and is gone. ---------
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Reproduces Moonbot's Settings dialog as a second face of the core-settings gear — eight tabs in Moonbot's own strip order — and makes it live wherever the safe-share wire reaches.
What is live
Every page draws EVERY row Moonbot has, including the ones this terminal cannot fill. A row it cannot fill is drawn in full and disabled, never hidden: a trader reads this window beside Moonbot's own dialog, and a missing row reads as a bug where a dead one reads as a limit. Each dead row states its reason in the source, and the reasons are distinct — the field is outside safe-share (Login's API keys, Remote's bot token and UDP password), or the wire carries it but no field means what the caption says, or it belongs to another surface's projection area.
Areas, not sections
An area is a PAGE, not a wire section. One wire field belongs to exactly one area, and a surface may write only what it drew — that is what stops an OK pressed on one page from reverting what a Moonbot user changed on another while the window stood open.
ExpertTab::add_sectionsfolds the mask over the tabs actually edited, so a window where only dead rows were touched sends nothing at all.The General page is the one place a Moonbot page splits in two: the compact gear popup draws its exits and blacklist and not the seven rows beside them, so those seven are their own area. Carrying them in the popup's mask would let its OK stamp them back from a frozen draft.
Refusals
Several rows are drawn dead on purpose, and the reason is recorded where the row is:
sign_ordersandjoin_sell_kindare marked on the wire as mirrors of the compactClientSettingsCommand; writing them here would set two routes fighting over one field.trading.free_position_checkis documented as CLOSING orphaned positions, not as the position-limit check the neighbouring caption names.pending_buy_price,h_pos_control, the order-book «Границы» slider and a few others have no wire field that means what their caption says. A mirrored control bound to the wrong field is worse than one that plainly does nothing.The echo path
The last commit fixes a defect the window's own traffic exposed.
SharedConfigSequenceconfirmed a write by comparing the WHOLE projection, so a concurrent core-side change to a field the write never named left a landed edit unconfirmed, re-sent the snapshot, and eventually dropped it asGaveUp. Confirmation, rejection and "the core already holds this" are now one mask-scoped comparison, and both it andapply_core_configdestructureFieldMaskso a bit added later cannot compile without an arm in each.Verification
cargo build --all-targetsandcargo fmt --checkclean;cargo test --workspacegreen — 1581 inmoon-core, 1423 in the UI, 273 contract tests. New tests pin the wire ordinals of the mouse-gesture and move-kind lists (their docs call them a display order, and a reorder would silently rewrite every core's stored gestures), the round trip of each new area against its NAMED wire slots, the popup's mask against the rows it does not draw, and the two echo regressions — each written before its fix and confirmed to fail without it.Not verified live: the window's own pixels and a real core's echo.