Skip to content

Vote Rewards: fix duplicate provider, broadcast tokens, layout#50

Merged
AdaInTheLab merged 1 commit into
mainfrom
feature/vote-rewards-cosmetics
Apr 30, 2026
Merged

Vote Rewards: fix duplicate provider, broadcast tokens, layout#50
AdaInTheLab merged 1 commit into
mainfrom
feature/vote-rewards-cosmetics

Conversation

@AdaInTheLab
Copy link
Copy Markdown
Collaborator

Summary

Three UX/correctness fixes spotted during the first real end-to-end test:

1. Duplicate provider entry on every server boot

The /vote chat command was printing two lines for one configured provider:

```
From Server: 7daystodie-servers: provider disabled
From Server: 7daystodie-servers.com: granted 100 points (balance: 100)
```

Cause: Newtonsoft.Json's default behavior with a property whose initializer returns a non-empty collection is to append JSON-loaded entries to the constructor-default list, not replace it. So:

```csharp
public List Providers { get; set; } = new List {
new VoteProviderSettings { Key = "7daystodie-servers", Enabled = false }
};
```

…meant every boot loaded the saved entry on top of the default, doubling the list. Fix: default `Providers` to empty, always run `EnsureDefaultProviders` after deserialization. The duplicate self-resolves on next boot — saved JSON has 1 entry; without the constructor pre-fill, deserialization yields exactly 1.

2. Broadcast hint rendered as "Tokens: , ."

vue-i18n was interpreting the literal `{player}` / `{reward}` in the translation as named-parameter slots, finding no params, and substituting empty strings. Fix: rename slots to `{playerToken}` / `{rewardToken}` and pass the literal token strings as parameters from the template.

3. Provider config was a tall single column

Wrapped each provider's fields in a 2-col CSS grid. Toggle / API key / broadcast template span the full row; Server ID + Poll Interval pair up; Reward Type + reward-value pair up. Mirrors the Discord tab's grid pattern. Collapses to 1 col under 768px.

Files

File What
`Features/VoteRewardsSettings.cs` Default `Providers` to empty list + comment explaining why
`Features/VoteRewardsFeature.cs` Always call `EnsureDefaultProviders` after deserialization (was only inside the persisted-state branch)
`frontend/src/views/SettingsView.vue` New `vote-provider-grid` 2-col layout + i18n params for broadcast hint + extracted broadcast placeholder const
`frontend/src/i18n/locales/{en,ja,ko,zh-CN,zh-TW}.ts` `{player}`/`{reward}` → `{playerToken}`/`{rewardToken}` in the broadcast hint

Test plan

  • Pull, build, deploy to dev server with rsync
  • Hard-refresh panel (`Ctrl+Shift+R`)
  • Settings → Vote Rewards: only one provider block visible (was two before)
  • Provider fields lay out in 2 columns at desktop width
  • Broadcast template hint reads "Tokens: {player}, {reward}. Empty = silent." (was "Tokens: , .")
  • `/vote` chat command prints exactly one provider line, not two
  • Resize to mobile width → grid collapses cleanly to 1 col

🤖 Generated with Claude Code

Three small UX/correctness fixes spotted during the first end-to-end
test against a real 7daystodie-servers.com vote.

1. Duplicate provider entry on every server boot
   Newtonsoft.Json's default behavior with a property whose initializer
   returns a non-empty collection is to APPEND the JSON-loaded entries
   to the constructor-default list, not replace it. So
     public List<VoteProviderSettings> Providers { get; set; } =
       new List<VoteProviderSettings> { new VoteProviderSettings { ... } };
   meant every boot loaded the saved entry on top of the default,
   doubling the list and causing /vote to print
     "7daystodie-servers: provider disabled"
     "7daystodie-servers.com: granted 100 points"
   side-by-side in chat. Fix: default Providers to empty, always run
   EnsureDefaultProviders after deserialization (idempotent backfill
   of the 7daystodie-servers default entry). The duplicate self-resolves
   on the next boot — saved JSON has 1 entry; without the constructor
   pre-fill, deserialization yields exactly 1.

2. Broadcast hint showed "Tokens: , ." instead of "Tokens: {player}, {reward}."
   vue-i18n was interpreting the literal {player} / {reward} in the
   translation as named-parameter slots, finding no params, and
   substituting empty strings. Fix: use distinct slot names
   ({playerToken} / {rewardToken}) in the translation and pass the
   literal token strings as parameters from the template.

3. Single-column form was unnecessarily tall
   Wrapped each provider's fields in a 2-col CSS grid, with the toggle,
   API key, and broadcast template spanning full-width. Server ID +
   Poll Interval pair up; Reward Type + reward-value pair up. Mirrors
   the Discord tab's grid pattern. Collapses to 1 col under 768px.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AdaInTheLab AdaInTheLab merged commit 85c71b7 into main Apr 30, 2026
2 checks passed
AdaInTheLab added a commit that referenced this pull request Apr 30, 2026
* Vote Rewards: dedupe provider list on load, idx-aware Vue key

PR #50 prevented new duplicates forming (the Newtonsoft-append bug),
but admins whose state was poisoned while that bug was active still
carry the doubled list in their persisted JSON. Symptoms after loading:

- /vote prints "<key>: provider disabled" + "<displayname>: granted ..."
  side by side because the foreach iterates both entries
- Settings tab shows two provider blocks; Vue's v-for warns about
  duplicate :key values and the diffing reconciles unpredictably

Two-part fix:

1. EnsureDefaultProviders now deduplicates by Key on every load. The
   entry with the strongest "configured" signal wins (enabled+apiKey >
   enabled > apiKey > anything else), so a real config never gets
   displaced by an empty stub. Logs a one-line note when it dropped
   any rows so admins can see it ran.

2. SettingsView.vue's v-for key is now `${provider.key}-${idx}` so
   duplicate provider keys can't collide in Vue's diffing. Defense in
   depth — the backend dedup means duplicates shouldn't reach the UI
   anymore, but the v-for shouldn't be the thing that breaks if they do.

Self-heals existing poisoned state on the next server boot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Vote Rewards: 2-col tab layout (config left, audit right)

The Vote Rewards tab was rendering everything in a single column inside
the left half of the available width — large empty space sat to the
right. Mirrors the Discord tab pattern: a 2-col grid at the tab level,
with the configuration cards (master toggle, providers, save) on the
left and the audit feed on the right.

Grid is 3fr / 2fr because the provider config card is wider than the
audit table needs. Collapses to single column at <=1100px (earlier than
the 768px tablet breakpoint) since the 3:2 split gets cramped on
narrow-desktop screens before mobile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@AdaInTheLab AdaInTheLab mentioned this pull request Apr 30, 2026
4 tasks
@AdaInTheLab AdaInTheLab deleted the feature/vote-rewards-cosmetics branch May 18, 2026 15:52
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.

1 participant