Skip to content

feat(hostd): replace per-price pin toggles with a single switch - #1539

Merged
ChrisSchinnerl merged 1 commit into
mainfrom
09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch
Sep 16, 2026
Merged

ChrisSchinnerl merged 1 commit into
mainfrom
09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch

Conversation

@telestrial

@telestrial telestrial commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Screenshot 2026-09-11 at 4.47.56 PM.png

This PR changes hostd's configuration page to support either pinning all pinnable fields or none of them, while still respecting the legacy mixed configuration. After this PR, it will not be possible through the UI to set a new mixed configuration. I think it's still possible to set a mixed config through the API, though?

Single field pinning toggles are gone. If you're in that legacy state, the overall pin toggle is set to the off position. I wrestled with that and can see arguments for having it in the on position. I'm open to whatever we think is best.

I tested this on my own host without saving anything and used clusterd beyond that, so more manual testing on a real hostd daemon would be useful before we release this into the wild.

@vercel

vercel Bot commented Sep 11, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
hostd Ready Ready Preview Sep 16, 2026 1:51pm UTC
4 Skipped Deployments
Project Deployment Actions Updated
explorer Ignored Ignored Preview Sep 16, 2026 1:51pm UTC
explorer-zen Ignored Ignored Preview Sep 16, 2026 1:51pm UTC
indexd Ignored Ignored Preview Sep 16, 2026 1:51pm UTC
renterd Ignored Ignored Preview Sep 16, 2026 1:51pm UTC

Request Review

@changeset-bot

changeset-bot Bot commented Sep 11, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 6b47dc5

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
hostd Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@telestrial
telestrial marked this pull request as ready for review September 11, 2026 21:39
Copilot AI lite review requested due to automatic review settings September 11, 2026 21:39

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Resolve mixed-state save behavior, pinning-disabled UI handling, effective-state validation, and Save availability for explicit unpinning.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

This PR replaces hostd’s per-price pin toggles with a single all-or-nothing switch while preserving legacy mixed configurations.

Changes:

  • Updates pinning transforms, UI, validation, and save-state handling.
  • Adds mixed-pinning notices and interaction tracking.
  • Adds unit/E2E coverage, fixtures, and a changeset.
File summaries
File Summary
libs/e2e/src/fixtures/textInput.ts Adds visibility assertions.
apps/hostd/contexts/config/usePinningTouched.tsx Tracks explicit pinning changes.
apps/hostd/contexts/config/useOnValid.tsx Handles pinning save behavior.
apps/hostd/contexts/config/types.ts Defines pinning state types.
apps/hostd/contexts/config/transform.ts Transforms aggregate and legacy pinning states.
apps/hostd/contexts/config/transform.spec.ts Tests transform behavior.
apps/hostd/contexts/config/index.tsx Exposes legacy pinning state.
apps/hostd/contexts/config/fields.tsx Updates pricing fields and validation.
apps/hostd/components/Config/PricingSetting.tsx Adds shared pricing controls and notices.
apps/hostd/components/Config/index.tsx Renders unified pricing controls.
apps/hostd-e2e/src/specs/configLegacyPinning.spec.ts Tests legacy mixed configurations.
apps/hostd-e2e/src/specs/config.spec.ts Updates pricing configuration tests.
apps/hostd-e2e/src/fixtures/configSetMixedPinning.ts Provides mixed-pinning setup.
apps/hostd-e2e/src/fixtures/configResetAllSettings.ts Updates configuration reset behavior.
.changeset/pin-all-prices-or-none.md Documents the user-facing change.
Review details

Suppressed comments (6)

apps/hostd/contexts/config/fields.tsx:264

  • In the mixed legacy state, this fiat input is rendered for a pinned storage price while values.shouldPinPrices is false. The short-circuit therefore accepts a negative pinned value and transformUpSettingsPinned can send it in the preserved mixed payload; validate against the effective per-price pin state instead (and apply the same correction to the other pinned-price validators).
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:313

  • In the mixed legacy state, this fiat input is rendered for a pinned egress price while values.shouldPinPrices is false. The short-circuit therefore accepts a negative pinned value and transformUpSettingsPinned can send it in the preserved mixed payload; validate against the effective per-price pin state instead.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:361

  • In the mixed legacy state, this fiat input is rendered for a pinned ingress price while values.shouldPinPrices is false. The short-circuit therefore accepts a negative pinned value and transformUpSettingsPinned can send it in the preserved mixed payload; validate against the effective per-price pin state instead.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:426

  • In the mixed legacy state, this fiat input is rendered for a pinned max-collateral value while values.shouldPinPrices is false. The short-circuit therefore accepts a negative pinned value and transformUpSettingsPinned can send it in the preserved mixed payload; validate against the effective per-price pin state instead.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/index.tsx:78

  • This notice is derived from the pinned response without checking pinningEnabled. Since the pinned resource is requested even when the explorer feature is disabled, a disabled host with a legacy mixed response can see an alert telling it to change pinning while shouldPinPrices itself is hidden. Include pinningEnabled in this condition so the notice is only shown when the host can act on it.
  const showMixedPinningNotice = hasMixedPinning && !pinningTouched

apps/hostd/contexts/config/usePinningTouched.tsx:24

  • Tracking touched state here is not enough to make the all-unpinned resolution savable. ConfigActions still disables Save when form.formState.isDirty is false; toggling a legacy mixed switch false → true → false marks this field touched but restores its default value, so isDirty becomes false. With no other edit, the host cannot submit the deliberate choice and the mixed configuration remains. Expose pinningTouched through the config context and include it in the save-enabled condition (or otherwise mark this action dirty).
    pinningTouched: !!touchedFields.shouldPinPrices,
  • Files reviewed: 15/15 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread apps/hostd/components/Config/PricingSetting.tsx Outdated
Comment thread apps/hostd/components/Config/index.tsx Outdated
@telestrial
telestrial force-pushed the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch from 2a8f7b5 to b1baabe Compare September 11, 2026 21:58
Copilot AI review requested due to automatic review settings September 11, 2026 21:58

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Mixed legacy pin validation and pinning-change tracking need correction before approval.

Review details

Suppressed comments (6)

apps/hostd/contexts/config/fields.tsx:264

  • For a mixed legacy host, transformDown sets shouldPinPrices to false even when this storage fiat input is still shown as pinned. This condition therefore short-circuits the non-negative check, allowing a negative value to be submitted for an active pinned price. Validate against the effective per-price pin state, including the legacy state, rather than only the aggregate switch.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:313

  • For a mixed legacy host, transformDown sets shouldPinPrices to false even when this egress fiat input is still shown as pinned. This condition therefore short-circuits the non-negative check, allowing a negative value to be submitted for an active pinned price. Validate against the effective per-price pin state, including the legacy state, rather than only the aggregate switch.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:361

  • For a mixed legacy host, transformDown sets shouldPinPrices to false even when this ingress fiat input is still shown as pinned. This condition therefore short-circuits the non-negative check, allowing a negative value to be submitted for an active pinned price. Validate against the effective per-price pin state, including the legacy state, rather than only the aggregate switch.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:426

  • For a mixed legacy host, transformDown sets shouldPinPrices to false even when this max-collateral fiat input is still shown as pinned. This condition therefore short-circuits the non-negative check, allowing a negative value to be submitted for an active pinned price. Validate against the effective per-price pin state, including the legacy state, rather than only the aggregate switch.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/transform.ts:313

  • For a mixed legacy host, transformDown deliberately sets shouldPinPrices to false, but the pinned fiat inputs for the individually pinned prices are still displayed. The range validators in fields.tsx now key off this aggregate flag, so a negative value entered into a displayed legacy-pinned field passes validation and is sent back with pinned: true; validate against the effective per-price pin state instead of the aggregate switch.
          shouldPinPrices:
            settingsPinned.storage.pinned &&
            settingsPinned.egress.pinned &&
            settingsPinned.ingress.pinned &&
            settingsPinned.maxCollateral.pinned,

apps/hostd/contexts/config/usePinningTouched.tsx:24

  • This treats a field blur as an explicit pinning change. FieldSwitch passes its onBlur into React Hook Form, so a user who merely tabs through the switch sets touchedFields.shouldPinPrices and the next unrelated save resolves a mixed configuration even though the switch value never changed. Track an actual checked-value change separately while retaining the on/off/on case.
    pinningTouched: !!touchedFields.shouldPinPrices,
  • Files reviewed: 15/15 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@ChrisSchinnerl ChrisSchinnerl left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at the deployment with Zeus as the backend and it looks great. I'll leave it up to Alex to judge code quality.

@alexfreska alexfreska left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, this looks great. Keeping the mixed config instead of silently resolving it is the right call, and the notice makes it clear what's going on.

I think it could be simplified a fair bit by loading a mixed config as shouldPinPrices: null instead of false. The switch still shows off, but the form itself then knows the host hasn't picked yet, so most of the legacy tracking can go.

It would also close one gap. Right now a host with a mixed config can't save to all unpinned from the switch alone. Flipping it on and back off leaves 0 changes and Save disabled, so they have to edit some unrelated field before they can save. With null as the starting value, switching to off counts as a change on its own.

Left some inline notes on where, plus a couple of ideas for making the notice push hosts to resolve it.

Comment thread apps/hostd/contexts/config/transform.ts Outdated
Comment thread apps/hostd/contexts/config/transform.ts Outdated
Comment thread apps/hostd/contexts/config/index.tsx Outdated
Comment thread apps/hostd/components/Config/index.tsx Outdated
Comment thread apps/hostd/components/Config/index.tsx Outdated
Comment thread apps/hostd/components/Config/PricingSetting.tsx Outdated
Comment thread apps/hostd/contexts/config/fields.tsx Outdated
Comment thread apps/hostd-e2e/src/specs/configLegacyPinning.spec.ts Outdated
Comment thread apps/hostd/contexts/config/types.ts Outdated
Copilot AI review requested due to automatic review settings September 15, 2026 17:23
@telestrial
telestrial force-pushed the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch from b1baabe to 35fa2ea Compare September 15, 2026 17:23

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

Claude Code Review is paused for this repository. To reconnect it, an admin of this repository's GitHub organization (or the account owner, for personal repositories) who can also manage your Claude organization's Code Review settings needs to re-link GitHub in Code Review settings. This is a one-time step.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved pinning data-loading/save behavior and validation issues must be addressed.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (3)

apps/hostd/contexts/config/fields.tsx:313

  • The same !values.shouldPinPrices guard skips range validation for the pinned egress input whenever the legacy mixed state sets the global value to null; a negative fiat value can therefore be submitted. Treat only false as the disabled case.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:361

  • The same !values.shouldPinPrices guard skips range validation for the pinned ingress input whenever the legacy mixed state sets the global value to null; a negative fiat value can therefore be submitted. Treat only false as the disabled case.
              !values.shouldPinPrices ||

apps/hostd/contexts/config/fields.tsx:426

  • The same !values.shouldPinPrices guard skips range validation for the pinned max-collateral input whenever the legacy mixed state sets the global value to null; a negative fiat value can therefore be submitted. Treat only false as the disabled case.
              !values.shouldPinPrices ||
  • Files reviewed: 13/13 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment thread apps/hostd/components/Config/index.tsx
Comment thread apps/hostd/contexts/config/types.ts Outdated
Comment thread apps/hostd/contexts/config/fields.tsx Outdated
Copilot AI review requested due to automatic review settings September 15, 2026 17:47
@telestrial
telestrial force-pushed the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch from 35fa2ea to 00018f1 Compare September 15, 2026 17:47

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved critical and moderate issues affect pinning saves and legacy mixed-state validation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

apps/hostd/contexts/config/fields.tsx:266

  • When shouldPinPrices is null for a legacy mixed configuration, this predicate validates the pinned value even for prices whose daemon-side pinned flag is false. The old per-price predicates skipped this check for those fields, so a mixed host can have a hidden, previously saved negative value that now blocks an unrelated save even though transformUpSettingsPinned intentionally preserves that configuration. Please make the range validation aware of the legacy per-price flags (or otherwise skip validation for unpinned legacy values) while still validating the price the user is actually choosing to pin.
              values.shouldPinPrices === false ||
              value?.gte(0) ||
              'storage price must not be negative',

apps/hostd/contexts/config/fields.tsx:315

  • In a legacy mixed configuration (shouldPinPrices === null), this validates the hidden egress pinned value even when the daemon reports egress as unpinned. The previous per-price condition skipped validation in that state, so an old negative value can block unrelated saves. Please use the daemon's per-price flag for this validator, or otherwise exempt unpinned legacy values while validating values the user chooses to pin.
              values.shouldPinPrices === false ||
              value?.gte(0) ||
              'egress price must not be negative',

apps/hostd/contexts/config/fields.tsx:363

  • In a legacy mixed configuration (shouldPinPrices === null), this validates the hidden ingress pinned value even when the daemon reports ingress as unpinned. The previous per-price condition skipped validation in that state, so an old negative value can block unrelated saves. Please use the daemon's per-price flag for this validator, or otherwise exempt unpinned legacy values while validating values the user chooses to pin.
              values.shouldPinPrices === false ||
              value?.gte(0) ||
              'ingress price must not be negative',

apps/hostd/contexts/config/fields.tsx:428

  • In a legacy mixed configuration (shouldPinPrices === null), this validates the hidden max-collateral pinned value even when the daemon reports max collateral as unpinned. The previous per-price condition skipped validation in that state, so an old negative value can block unrelated saves. Please use the daemon's per-price flag for this validator, or otherwise exempt unpinned legacy values while validating values the user chooses to pin.
              values.shouldPinPrices === false ||
              value?.gte(0) ||
              'max collateral must not be negative',
  • Files reviewed: 13/13 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread apps/hostd/contexts/config/transform.ts
Copilot AI review requested due to automatic review settings September 15, 2026 18:33
@telestrial
telestrial force-pushed the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch from 00018f1 to 5611d8c Compare September 15, 2026 18:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A failed pinned-settings request can allow persistence while reporting a save error and leaving the form dirty.

Review details

Suppressed comments (1)

apps/hostd/contexts/config/useOnValid.tsx:50

  • When the pinned-settings GET has failed, this guard skips the pinned PUT, but onValid still calls revalidateAndResetForm after the settings PATCH. With explorer enabled, that function treats the missing _settingsPinned response as an error, so the settings change is already persisted while the user sees an error and the form remains dirty. Either block the save until pinned settings are available or make the revalidation path handle this intentional no-data case without reporting a partial-save failure.
        if (state.data?.explorer.enabled && resources.settingsPinned.data) {
  • Files reviewed: 14/14 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copy link
Copy Markdown
Contributor Author

Went ahead and implemented the null approach. Thank you, @alexfreska! A good amount of reduced complexity in that suggestion.

@alexfreska alexfreska left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes look great. One small thing: a few files aren't Prettier formatted (components/Config/index.tsx, contexts/config/transform.ts, contexts/config/transform.spec.ts), so maybe run npx prettier --write on those before merging. Otherwise good to go.

@telestrial
telestrial force-pushed the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch from 5611d8c to 6b47dc5 Compare September 16, 2026 13:49
Copilot AI review requested due to automatic review settings September 16, 2026 13:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@ChrisSchinnerl
ChrisSchinnerl merged commit 00c9de5 into main Sep 16, 2026
54 of 55 checks passed
@ChrisSchinnerl
ChrisSchinnerl deleted the 09-11-feat_hostd_replace_per-price_pin_toggles_with_a_single_switch branch September 16, 2026 14:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants