← Home

Changelog

Curated highlights of what’s shipped recently. The full history lives on GitHub.

  1. May 2026

    Wire 13 casino games to their reserved achievement events

    Follow-up to #520. That PR reserved 13 hidden roadmap stubs in `AchievementCatalog` for casino games that didn't yet publish domain events. This PR wires them up so every stub is `hidden = false` and `PENDING_HIDDEN_CODES` is empty. ### What changed **13 new event classes** under `common.events` — one per game, mirroring `BlackjackNaturalEvent`'s minimal `(discordId, guildId)` shape (Highlow adds an `isWin` flag). **13 modified services** under `database.service` — each ge…

    #522
  2. May 2026

    Public profiles via leaderboard click-through + level pill alongside title

    Turns leaderboard rows into links to a read-only public profile so members can compare their level, streak, and achievements with anyone else in the guild — and surfaces each member's **level next to their title** across the leaderboard for at-a-glance prestige. **Public profile route** `GET /profile/{guildId}/{targetDiscordId}` reusing `WebGuildAccess.requireForPage` + `ProfileWebService.getProfile` (already keyed by `(discordId, guildId)`). Self-clicks bounce to the exis…

    #521
  3. May 2026

    Expand catalog with new tiers, counters, consolation, and roadmap stubs

    Expands the achievements catalogue with **19 new visible specs** + **13 hidden roadmap stubs**, riding entirely on event listeners that already exist in `AchievementEventHandler`. No new domain events. ### What's new (visible) **Streak ladder**: `streak_100` (Centurion), `streak_365` (Year of Toby) **Level ladder**: `level_75` (Elite), `level_100` (Legend) **Duel wins ladder**: `duel_wins_25` (Champion), `duel_wins_50` (Warlord), `duel_wins_100` (Undefeated) **Voice ladder…

    #520
  4. May 2026

    Surface achievements and notifications on the homepage

    Add two feature-cards to the existing **Engagement** section of `home.html`: **Achievements** (links to `/profile/guilds`) and **Notifications** (links to `/preferences/notifications`). Engagement section grows from 4 to 6 cards, matching the Casino section's 6-card layout — no CSS changes needed. Extend `HomeStatsService.HomeStats` with `achievementCount` and `notificationKindCount`, sourced directly from `AchievementCatalog.all.count { !it.hidden }` and `NotificationChan…

    #519
  5. May 2026

    Admin tabs no longer paint over the navbar / drawer / dropdowns

    The previous attempts (#509 layout, #510 raising the drawer to `z-index: 1000`) didn't fix admin tab overlap because the cause wasn't z-index — it was selector scope. `nav { position: relative; z-index: 10 }` in `nav.css` matched **every** `<nav>` on the page, including `<nav class="mod-subnav">` in `moderationHeader.html`. That made the admin tab strip create its own root-level stacking context at the same z-index as the top navbar but later in the DOM, so it painted over…

    #516
  6. May 2026

    Web buy/sell watches on the /economy market page

    Surfaces the Discord `/pricealert` feature (#512) on the web dashboard. A watch created from `/economy/{guildId}` writes to the same `user_price_trigger` table the scheduler scans on each market tick, so the firing path is unchanged and either surface can manage the other's rows.

    #514
  7. May 2026

    /pricealert auto-trade triggers with PRICE_ALERT receipt DM

    Wires up the `PRICE_ALERT` notification kind, which previously had infrastructure (channel kind, router DM/PUSH surfaces) but no trigger source. Adds `/pricealert add|list|remove`. A trigger captures the **current market price** and a **target threshold** plus the user's chosen side (BUY/SELL) and amount. On every 5-min tick, `TobyCoinPriceTickJob` scans enabled triggers and fires any whose target was reached **from the side the price was on at creation** — so "buy at 100…

    #512
  8. May 2026

    /jackpotadmin lottery_refresh_embed — manual embed rebuild trigger

    ## Why

    #511
  9. May 2026

    Nav drawer stacking, additive embed rebuild, top holders include bonus tickets

    ## Three bugs reported after #509

    #510
  10. May 2026

    Tab-nav layout + silent web bonus award + broken bulk-buy hint

    ## Three bugs reported after PR #508 merged

    #509
  11. May 2026

    Surface incentives on the player page + polish moderation pages

    ## Why

    #508
  12. May 2026

    Drop broken icon asset from sw.js so macOS Firefox renders

    macOS Firefox test push from `/api/push/test` showed the full happy path server-side: ``` WebPushAdapter: delivering to 2 endpoint(s) for 313049624636162059. WebPushAdapter: HTTP 201 from .../wpush/v2/gAAAAABqDCEy…; marking used. WebPushAdapter: HTTP 201 from .../wpush/v2/gAAAAABqDFP2…; marking used. POST /api/push/test … status=200 ``` …but no banner ever rendered on the laptop. The next log line is the smoking gun: ``` GET /images/toby-icon.png … status=404 ``` `sw.js` s…

    #507
  13. May 2026

    Reward buying more tickets via three configurable incentives

    Some users had figured out that buying a single TICKET_WEIGHTED lottery ticket was enough to satisfy `LOTTERY_DAILY_MIN_BUYERS` and be entered — making one-ticket entries the optimal play. Rather than punishing that (e.g. raising minimums), this PR adds three additive, positive incentives that make committing more attractive than splitting. Existing `LOTTERY_DAILY_MIN_BUYERS` safeguard is untouched. All three levers are default-off so nothing changes for existing guilds un…

    #506
  14. May 2026

    SOLID/DRY sweep across common, core-api, database, web

    Top-6 focused SOLID/DRY refactor + test-gap sweep spanning all four priority modules. Each refactor lands with the tests it enables; behaviour is preserved (no public API breaks). **Result:** 19 modified / 10 new files. Net **−221 LOC** across modified files. **+44 new tests** (common 9, core-api 13, database 5, web 17). **All 1,704 buildable-module tests pass.**

    #505
  15. May 2026

    Register BouncyCastle eagerly + add push-path observability

    After PR #503, browser push still wasn't reaching users. Heroku logs from a real attempt revealed: ``` WARN bot.toby.notify.WebPushAdapter - WebPushAdapter: send threw for https://updates.push.services.mozilla.com/wpush/v2/…: no such provider: BC ``` Browser subscription, persistence, prefs, dispatch, and adapter wiring were all healthy — but `DefaultPushTransport.send()` builds `Notification(endpoint, p256dh, auth, body)` **before** touching its lazy `pushService` field,…

    #504
  16. May 2026

    Wire WebPushAdapter via @Autowired constructor

    Setting `TOBY_VAPID_PUBLIC_KEY` + `TOBY_VAPID_PRIVATE_KEY` on Heroku crashed the dyno (`Process exited with status 1`, then `H10 App crashed` on every request). Root cause: `WebPushAdapter` has two constructors and neither was `@Autowired`. Spring defaults to the **primary** for Kotlin `@Component`s, which needs a `PushTransport` bean — but `PushTransport` is just an interface and `DefaultPushTransport` is a plain class with no `@Component` / `@Bean` factory. With the env…

    #503
  17. May 2026

    Align admin DM/channel settings with notification prefs

    Three seams between the older admin pages (per-feature channel pickers, "DM-only" toggles) and the per-surface notification-preferences system that landed in V37. **Achievement shoutout label was misleading.** The admin UI said *"Leave blank to keep unlocks DM-only"*, but `AchievementEventHandler.onAchievementUnlocked()` always fires the DM (gated by the user's `ACHIEVEMENT_UNLOCK`/`Surface.DM` pref) and the channel post is additive. Updated the hint text and the empty-opt…

    #502
  18. May 2026

    Auto-route /preferences + /preferences/notifications past the picker when anchored

    Follow-up to #496. The Settings and Notifications navbar entries from that PR landed users on the `/preferences` hub or the notifications guild-picker even when they had already anchored a default guild via `DefaultGuildCookie`. That's an extra click vs. the rest of the app — `/leaderboards`, `/profile/guilds`, `/casino/guilds`, `/intro/guilds` etc. already skip their pickers using the shared `DefaultGuildRedirect.pick(...)` helper. This PR applies the same helper to both…

    #499
  19. May 2026

    Ratchet level/streak progress + regroup /achievements display by category

    Two complaints addressed: 1. **Progress on `level_*` (and `streak_*`) achievements is wrong.** The catalog gives these milestones `threshold = 5/25/50` (and `3/7/30`), so the locked display tries to show `(progress/threshold)` — but the handler called `achievementService.unlock(...)` on milestone-cross, which skips the progress table entirely. Result: locked level/streak entries always rendered as `(0/5)`, `(0/25)`, etc., regardless of the user's actual level or streak. Co…

    #498
  20. May 2026

    Surface settings + notification preferences in navbar

    The notification-preferences feature shipped end-to-end in #491/#493 (backend service, REST endpoints in `EngagementApiController`, MVC controller, templates, JS), but the only navbar path to `/preferences` was the small default-server star chip in the nav-identity block — whose label is the current default-server name and whose tooltip says "click to change default server." Nothing in the navbar said Settings, Preferences, or Notifications, so the matrix was effectively i…

    #496
  21. May 2026

    Wire Web Push adapter for Surface.PUSH

    Replaces the no-op `sendPush` stub from #493 with a real Web Push (RFC 8030 / 8291 / 8292) delivery path. End-to-end: schema → DTO/persistence/service → adapter → router wiring → REST endpoints → service worker → toggle UI on `/preferences/notifications`. Without VAPID keys configured the adapter bean is not registered (`@ConditionalOnProperty` + `@ConditionalOnBean`), so `NotificationRouter` falls back to the existing one-shot "no adapter wired" warn path. Dev/CI environm…

    #494
  22. May 2026

    Per-surface notification preferences + push stub + blackjack_natural hookup

    Today every `user_notification_pref` row is keyed by `(discord_id, guild_id, channel_kind)` — one flag per (user, kind). That collapses DM and channel behaviour into a single toggle for kinds that have both (today only `ACHIEVEMENT_UNLOCK`), and channel posts ignore prefs entirely so tip recipients / duel opponents / lottery winners get pinged regardless of preference. This PR splits the row by surface so a user can opt out independently per surface. Also bundles two defer…

    #493
  23. May 2026

    Centralise channel routing through NotificationRouter

    Follow-up to #491. Pulls channel resolution + permission checks out of each notifier and into a single `NotificationRouter.sendChannel(...)` method, gated by a per-route `ChannelRouteKey` enum. Same shape as the existing `sendDm(...)` — one delivery gate per surface, two surfaces (DM, CHANNEL). Also fixes the latent `INTRO_PROMPT` preference bypass: `IntroNotificationService.promptUserForMusicInfo` used to DM unconditionally; now it honours the per-user opt-out.

    #492
  24. May 2026

    Daily streak, achievements, notification preferences

    Adds a cohesive **engagement loop** on top of the existing per-guild XP/credit infrastructure: **Daily streak** (`/daily`): claim once per UTC day to grow a per-(user, guild) streak. Rewards scale by streak length (`min(base + per_day_bonus × (streak − 1), max)`) and bypass the daily XP/credit caps. Publishes `StreakClaimedEvent`. **Achievement engine** (`/achievements`): seeded from `AchievementCatalog` on startup. Counter-based (`progress(code, delta)`) and one-shot (`un…

    #491
  25. May 2026

    Trim low-value misc commands

    You flagged `/hellothere`, `/ch`, and `/brother` as low-value. This PR removes the first two outright and reworks `/brother` into something a non-Toby guild would actually use. **Delete `/hellothere`** — one-line Star Wars joke that only does anything if you happen to type a date relative to 2005-05-19. No replay value. **Delete `/ch`** — text-mangling gimmick with a stale `ink`/`gamerword` censoring easter egg. **Rework `/brother`** as a real registry: `check` — preserves…

    #490
  26. May 2026

    Restore desktop navbar identity/auth + dedupe drawer close ✕

    Three follow-ups to the navbar redesign in #488 (now on `main`): **1. Desktop regression — Login + default-guild pill were invisible.** The redesign moved Login into `.nav-auth` and the username + default-guild pill into `.nav-identity`, then set both to `display: none` on desktop. Result: logged-out users couldn't see the Login button; logged-in users couldn't see their default-server pill or Logout button on desktop. Removed both from the desktop hide-list and gave them…

    #489
  27. May 2026

    Redesign mobile navbar as a slide-in drawer

    The mobile navbar was broken (screenshot below): the hamburger ended up *below* the open menu because `.nav-toggle` sat after `.nav-links` in the DOM with `flex-wrap: wrap`. The drawer felt detached from the page (no backdrop, no scroll lock, no close affordance) and every link looked identical with no grouping or active-page indication. **Mobile redesign:** Hamburger moved before `.nav-links` so it can no longer wrap below the open menu. `.nav-links` becomes a fixed-posit…

    #488
  28. May 2026

    Add pick=true to remaining "All servers" back-links

    Adds `?pick=true` to the "← All servers" back-link on 11 per-guild pages (poker-lobby, blackjack-lobby/solo, lottery, profile, economy, titles, tip, duel, music-player, moderation). Without the query param the picker auto-redirect introduced in #486 bounces the user straight back to the page they just left, leaving them no way to re-anchor a different server from page chrome. Adds the back-link to `intros.html`, `excuses.html`, `teams.html` which had no in-page change-serv…

    #487
  29. May 2026

    Auto-route through guild pickers when possible

    Skips the `/{thing}/guilds` picker when the user only shares one server with the bot, or has anchored a default server. Applies to every picker in the navbar (casino games, leaderboards, intros, poker, blackjack, lottery, profile, economy, titles, tip, duel, music, excuses, teams, moderation). Each picker card grows a `☆ Set as default` / `★ Default` toggle, the navbar shows a "★ ServerName" pill when anchored, and a new `/preferences` page lets the user manage the anchor…

    #486
  30. May 2026

    Reorganize moderation settings UI into Server/Economy/Casino pillars

    A re-arranging pass on `/moderation/{guildId}/settings` to fix two awkwardnesses without rewriting the page: **Blackjack and Poker each had settings split across two homes.** Table rules lived in their own section, but ante/blind/buy-in stakes were buried inside the "Casino stakes & buy-ins" mega-section — tuning either game meant scrolling between two cards. Stakes now live with their game. **No top-level grouping.** Three `<h2 class="config-pillar">` headers (**Server**…

    #485
  31. May 2026

    Render full progress bar on level-up embed

    The Progress field on the level-up embed was computed via `LevelCurve.progress(totalXp)`, which returns `xpIntoLevel = 0` when the user lands exactly on the level threshold. That made the celebration message render an empty bar (e.g. `░░░░░░░░░░ 0 / 155 XP`) right at the moment of leveling up — the "wonky" look in the screenshot. Switch `buildLevelUpEmbed` to use `LevelCurve.xpForNextLevel(newLevel - 1)` as both `filled` and `total` for the bar, so the achieved level rende…

    #484
  32. May 2026

    Gate purchases by level, add level-ladder titles, extend tiers

    **Gate purchasable titles by level.** `TitleDto.required_level` (added in V34) is now enforced on the buy paths — Discord `/title buy`, web `/titles/{guildId}/{titleId}/buy`, and the one-click "Buy with TOBY" flow. Logic is centralised in a new `TitlePurchasePolicy` so all three callsites share the same rule. The auto-unlock branch on level-up (`LevelUpListener.unlockTitles`) is unchanged. **Add 14 level-ladder titles (V35).** New titles span Lvl 5 → Lvl 200 (🌱 Sprout … ♾…

    #483
  33. May 2026

    Award XP for web casino/poker/duel/economy actions

    Web gameplay surfaces (casino, blackjack, poker, duels, lottery, the economy market, tips) previously bypassed the XP system entirely — playing slots in Discord awarded 5 XP per slash invocation, but the same action through the browser awarded zero. This change closes the gap. New `WebGameplayXpInterceptor` — a Spring `HandlerInterceptor` that, after a successful (2xx) POST to a gameplay/economy route, calls `XpAwardService.award(...)` with the same 5 XP grant Discord uses…

    #481
  34. May 2026

    Render Discord level-up as a tier-colored embed

    Discord level-up announcement is now a tier-colored embed that mirrors the web profile card (`templates/profile.html` + `static/css/profile.css`): tier color (Bronze/Silver/Gold/Diamond), level badge in the title, an XP progress bar, and total lifetime XP. Numbers come from `LevelCurve.progress(totalXp)` — the same source the profile page uses — so the embed and the profile show the exact same `xpIntoLevel` / `xpForNextLevel` / total at the moment of the level-up. `LevelUp…

    #480
  35. May 2026

    Dedicated tab + role-reward CRUD + title gating

    Closes the moderation-UI gaps from #477. That PR shipped the leveling backbone (XP, levels, `level_role_reward` table, `title.required_level` column) but the admin UI was incomplete: the four config keys were buried inside the collapsed-by-default "Economy" `<details>`, the `level_role_reward` table had no CRUD path, and `title.required_level` could only be set via direct DB write. ### Why this was missed `ModerationTemplateRowsTest` only enforces that every `ConfigDto.Con…

    #479
  36. May 2026

    Collapse navbar dropdown sections on mobile

    On a phone-width viewport (≤600px), the navbar hamburger opened a menu where all four dropdown sections (Play, Economy, Social, Tools) were already fully expanded — every link visible, scroll forever, and the section toggle buttons did nothing. Root cause was one CSS rule in the mobile media query of `nav.css`: `.nav-dropdown-menu { display: flex; ... }` was set unconditionally, overriding the desktop default (`display: none` unless `.open` is on the parent). The `toggleDr…

    #478
  37. May 2026

    Add per-guild XP, levels, role rewards and title gates

    Adds a leveling / engagement system on top of the existing social-credit infrastructure. XP is tracked **separately** from social credit so spending or losing credit at the casino doesn't drag a user's level down — the long-term progression track is durable. ### XP awards (all daily-capped, default 1000 XP/day per user/guild) **Messages**: 15–25 XP per non-bot message with a 60s per-user cooldown (Tatsu/MEE6-style anti-spam), tracked in a Caffeine cache keyed on `(guildId,…

    #477
  38. May 2026

    Resume preempted audio after intro plays

    Today, when a member joins voice while the bot is already playing audio, their intro is enqueued **behind** the currently-playing track and only plays after that track finishes — defeating the point of an intro. This PR makes the intro preempt the currently-playing audio and resume it (from the same position) once the intro finishes.

    #476
  39. May 2026

    In-browser preview of search results and queue items

    The /music-player dashboard's search panel lists up to 10 results per query, but the only way to find out which "Linkin Park — Numb" is the right one is to queue it and let the bot play. Same gap on items already queued — title/author visible, no way to sanity-check before they reach the front. This PR adds a small **▶ Preview** button next to each search result and each queue item that plays the source's own ~30s clip directly in the browser via a single shared `<audio>`…

    #475
  40. May 2026

    Expand Lavaplayer sources + polish embed + web dashboard

    Three connected pieces shipped together: ### A — Expanded audio sources Registers **SoundCloud, Bandcamp, Vimeo, Niconico** (built-in Lavaplayer) and **Spotify, Apple Music, Deezer, Yandex Music** (via the LavaSrc 4.8.2 plugin) alongside the existing YouTube/Twitch/HTTP/Local sources. Each LavaSrc source is gated on its env-var credentials (`SPOTIFY_CLIENT_ID`/`SPOTIFY_CLIENT_SECRET`, `APPLE_MUSIC_MEDIA_API_TOKEN`, `DEEZER_MASTER_KEY`/`DEEZER_ARL`, `YANDEX_ACCESS_TOKEN`);…

    #472
  41. May 2026

    Derive homepage game counts from a single GameCatalog

    The homepage was rendering three different game counts because the stats strip, hero paragraph, and casino feature card each held their own hardcoded literal: | Location | Before | |---|---| | Stats strip (`home.html` line 39, `homeStats.gameCount`) | `12` (from `HomeStatsService.GAME_COUNT`) | | Hero meta description + paragraph (`home.html` lines 5, 16) | `13`-game casino | | Casino feature card body (line 64) | `12` quick-play minigames | | Casino feature card tag (line…

    #474
  42. May 2026

    Add Ko-fi link across web, README and Discord bot

    Surfaces a single Ko-fi link (`https://ko-fi.com/fratlayton`) in one spot per user-facing channel so supporters can find it without making the bot feel like a paywall: **Web footer** (`fragments/footer.html`) — appears on every marketing page (home, commands, terms, privacy, changelog). **Home page final CTA** — small `hero-aside` link under the existing Invite / Browse buttons. **README.md** — new "Support the Project" section above Acknowledgments. **`/help` no-args repl…

    #473
  43. May 2026

    Redesign section tabs as segmented cards with counts

    The Members / TobyCoin / Games tab strip shipped in #470 cloned the `.lb-sort` pill aesthetic (designed for a secondary inline switcher) and read as small + left-aligned + visually lost between the prominent podium above and the prominent tables below. This PR redesigns the section nav as a full-width row of equal-flex segmented cards echoing the existing `.lb-stat` / `.lb-podium-card` aesthetic — `bg-elevated` + border + `radius-lg`, with the accent fill on the active tab…

    #471
  44. May 2026

    Tabbed sections, month nav, hover contributors, deltas, sparklines

    The leaderboard page used to stack three full sections vertically (credit standings, TobyCoin wallets, Top Games) and the Top Games list collapsed each game to a single number. This PR reshapes the page into three tabs and gives the Games panel real depth. ### What changed **Tabs** above the fold: **Members · TobyCoin · Games**. The podium stays as the page hero above the tab strip. Tab choice is persisted per guild in `localStorage` (matches the existing collapse-state me…

    #470
  45. May 2026

    Modal-driven /team split with preview-confirm flow + web presets

    Rebuilds `/team` around a Discord modal, a preview-then-confirm flow, and a `/teams/{guildId}` web UI for saved rosters. Replaces the old one-shot slash command that immediately created voice channels with no chance to review. **Discord flow** `/team split` opens a modal (optional `members:` slash-option pre-fills it) Modal collects: optional preset name, members (paragraph; `@mentions` or raw IDs), team count, naming strategy (`prefix`|`list`), names Submit shows an ephem…

    #469
  46. May 2026

    Add ban/timeout/purge/lock/slowmode tools + Actions UI tab

    Replaces `/shh` and `/talk` (and their voice-tab Shh/Talk buttons) with a proper moderation toolkit. Every new action is reachable both as a Discord slash command and from a new **Actions** tab on the moderation dashboard. ### New slash commands (`discord-bot`) | Command | What it does | |---|---| | `/ban` | Ban one or more mentioned members (optional reason + 0-7 days message delete) | | `/unban` | Unban a user by Discord ID | | `/timeout` | Discord-native timeout, 1-4032…

    #468
  47. May 2026

    Mobile-friendly pass, breakpoint refactor, responsive tests

    Full mobile-friendliness sweep across the dashboard. An audit found ~25 concrete mobile bugs across 15 casino games + admin/marketing pages, inconsistent breakpoints (480 / 600 / 640 / 380 px scattered across 31 CSS files), and the same `repeat(N, 1fr) → 1fr` reflow pattern duplicated dozens of times in casino game CSS. This pass standardises the breakpoint set, extracts shared utilities, fixes the bugs, and pins the contract with tests so future regressions surface in CI.…

    #466
  48. May 2026

    Align admin poll page with /poll modal (4 options, 1 min)

    `/poll` was reworked into a Discord modal (`PollModal.kt`) that — bound by Discord's 5-component modal cap — accepts a paragraph question (1–500 chars) plus up to **4** single-line options, with only option 1 required. The admin web page at `/moderation/{guildId}/poll` was still built for the old shape (max 10 options, 2 required, 256-char single-line question), so it was visibly out of sync and could submit polls that needed shapes the new emoji list couldn't represent. T…

    #467
  49. May 2026

    Revert YouTube proxy from LavaPlayer + Ktor paths

    LavaPlayer was 407-ing every CONNECT through the YouTube proxy across all configured clients, surfacing in production as: ``` dev.lavalink.youtube.AllClientsFailedException: (yts.version: 1.18.0) All clients failed to load the item. Client [TVHTML5_SIMPLY] failed: Invalid status code for fetch player script (embed): 407 Client [WEB] failed: Invalid status code for client config fetch: 407 Client [ANDROID] failed: Invalid status code for player api response: 407 ``` Every v…

    #465
  50. May 2026

    Authenticate HTTPS CONNECT to YouTube proxy

    Intro preview was logging `YouTube Data API preview returned HTTP 407` on every URL pre-flight when the YouTube proxy was configured with auth. Root cause: `IntroWebService.openYouTubeDataApiConnection` set `Proxy-Authorization` via `HttpURLConnection.setRequestProperty`. That header is only attached to the inner HTTP request inside an established CONNECT tunnel — it isn't sent on the CONNECT itself. So for HTTPS targets like `googleapis.com`, the proxy 407s the tunnel bef…

    #464
  51. May 2026

    Swap per-horse odds label when Win/Place/Show changes

    The horse cards always rendered the WIN multiplier regardless of which bet type was selected, so the favourite looked like "3.1× win" even when Place was active (where it actually pays 1.7×). Server-render all three multipliers as `data-{win|place|show}-mult` attributes on each `.hr-horse` card. New `updateHorseOdds(cards, betType)` helper rewrites the visible label when the bet radio changes. IIFE listens for `change` events on every `input[name="bet"]` radio and primes t…

    #463
  52. May 2026

    Add Horse Racing minigame (Win/Place/Show on 6-horse field)

    New casino minigame: `/horse-racing` slash command + `/casino/{guildId}/horse-racing` web page with an animated six-lane track. Six horses, named with distinct win odds (favourite H1 to longshot H6); three bet types — **Win** (1st only), **Place** (top 2), **Show** (top 3). **Pure logic** (`HorseRacing.kt`) uses Plackett–Luce sampling: each horse's "strength" is its stated win probability, so favourites land on the podium more often than just their win rate suggests — exac…

    #462
  53. May 2026

    Add Plinko + Wheel of Fortune (low-RTP jackpot feeders)

    Two new jackpot-eligible casino games that sit in the sub-92% RTP band (where only Slots, Scratch, and Dice currently live). Both fund the per-guild jackpot pool via loss tribute and roll for it on a win — and unlike Coinflip / Blackjack / Baccarat / Roulette, they're tight enough on RTP that the `JACKPOT_RTP_MAX_PCT` gate will still let them roll when a guild dials the ceiling down. ### Plinko (canonical RTP ≈ 0.89) 8 peg rows, 9 buckets. Three risk profiles (`LOW`, `MEDI…

    #461
  54. May 2026

    Honor [hidden] on .btn-primary and .btn-secondary

    Follow-up to #458. The user noticed the previous fix didn't actually hide the "Buy (sell N TOBY)" button on scratch even when their balance comfortably covered the stake (screenshot: balance ~100k, stake 100, button still visible reading "sell 0 TOBY"). Same story for the "Reveal all" button on initial page load. **Root cause:** `.btn-primary` and `.btn-secondary` in `base.css:118-152` set explicit `display: inline-flex`, which outranks the user-agent stylesheet's `[hidden…

    #460
  55. May 2026

    Load casino-win-settle.js on the bespoke web pages

    User report: "no coins visible" on `/blackjack/{guild}/solo` after a winning hand — the chip-stack flourish that other casino pages drop on the felt never appeared. Root cause: #428 swapped `blackjack-solo.js`'s `CasinoRender.flashChipsOn(...)` call for the new shared helper `TobyCasinoWinSettle.fire(...)` and hoisted `casino-win-settle.js` into the shared `casinoMinigame :: scripts` fragment. But `blackjack-solo.html` (and `blackjack-table.html`) hand-roll their `<script>…

    #459
  56. May 2026

    Split into subcommand-per-category modals

    Splits the monolithic `/setconfig` slash command into 12 subcommands, each opening a category-scoped modal pre-populated with the guild's current values. Replaces the previous 25-flat-option form that was hitting Discord's hard option cap and forcing ~45 other config keys to be web-only.

    #457
  57. May 2026

    Lock buy buttons through reveal + refresh TOBY button on balance changes

    Two scratch-card UX gaps that diverged from how the other casino games (slots, dice, coinflip, keno, etc.) behave: **Buy buttons re-enabled mid-reveal.** The "Buy ticket" / "Buy (sell N TOBY)" buttons flipped back to enabled the moment the server returned a card — before the player had finished scratching cells. A fast-clicker (or autoclicker) could submit a second buy on top of a half-revealed card. Fix: convert scratch's `renderResult` from a no-op to a Promise resolved…

    #458
  58. May 2026

    SOLID/DRY pass + expand modal usage

    Three workstreams in one PR: ### A. Discord-bot SOLID/DRY **`Loggable` mixin** in `core-api/core/log` — `Command`, `Button`, `Menu`, `Modal` each used to declare an identical `val logger: DiscordLogger get() = DiscordLogger.createLogger(this::class.java)`. Now one interface. **`PermissionValidator`** + `DefaultPermissionValidator` (`actorMayActOn` / `botMayAct`) — `MoveCommand` and `KickCommand` migrated. Removes inline `canInteract`/`hasPermission` duplication. **`WagerCo…

    #456
  59. May 2026

    Tighter walk-and-shoot choreography + mobile-friendly overlay

    Two follow-up tweaks to the duel resolution animation that merged in #453. **Choreography.** The figures now begin overlapping at the arena centre and walk apart back-to-back. The winner half-flips (`scaleX(-1)`) just before the flash, and the loser falls without flipping. Reads as the classic Western duel arc — together, walk apart, turn, shoot — instead of the previous "just start a bit close and walk out" beat. The flip lives on `.duel-figure-avatar` (the disc only) rat…

    #454
  60. May 2026

    Play a resolution animation when an offer is accepted on the web

    Follow-on to #452 (which enriched the recipient + per-offer expiry on the web inbox). When the acceptor clicks **Accept**, the silent toast is replaced with a modal overlay that animates the two duelists: they walk apart, a 💥 flashes between them, the loser tips over, the winner gets a gold glow, and the `+pot credits` pill flies to the winner. Click anywhere, hit <kbd>Esc</kbd>, or wait 6s to dismiss — at which point `refreshAll()` runs and the inbox/balance update norma…

    #453
  61. May 2026

    Show opponent name + avatar and per-offer expiry on web inbox

    The web `/duel/{guildId}` page previously showed each pending offer's recipient as a raw 18-digit Discord ID (e.g. `123456789012345678`) and gave no per-row expiry indication — only a static "offers expire after 3m" footer. This PR enriches `PendingDuelView` so both the outgoing and incoming offer lists render the member's server nickname and avatar and a live countdown of how long the offer has left. `DuelWebService` now injects `MemberLookupHelper` (same pattern Poker/Bl…

    #452
  62. May 2026

    Centralised searchable user picker for every user dropdown

    Every form that lets you pick a member — tip recipient, duel opponent, excuse attribution, intro management, voice move targets, casino jackpot refund — used a plain `<select>` populated with `${members}`. On servers with lots of members those dropdowns are a long scroll. This PR replaces all of them with a single searchable typeahead component. New fragment `templates/fragments/userPicker.html` — one signature handles single-select, multi-select with chips, optional pre-s…

    #450
  63. May 2026

    Resolve author to current Discord name at display time

    Follow-up to #443. The previous PR stored `author_discord_id` at submission but kept rendering the **snapshot** `author` string everywhere, so a user changing their server nickname left stale names attached to old excuses. This PR resolves the author through JDA at display time. Render ladder, applied identically by `ExcuseCommand.resolveDisplayAuthor` (slash) and `ExcuseWebService.resolveDisplayAuthor` (web): 1. `guild.getMemberById(authorDiscordId)?.effectiveName` — pick…

    #447
  64. May 2026

    Render monster and equipment images in lookups

    The dnd5eapi exposes an `image` field on **monsters** (e.g. `/api/images/monsters/goblin.png`) and **equipment** (e.g. `/api/images/equipment/longsword.png`). Other endpoints don't. Discord embeds for those two types now set a **thumbnail** (corner image), keeping the dense HP/AC/actions fields readable. The web `/dnd` card now renders an **inline `<img>`** floated right on desktop, centred on mobile. Both fall back gracefully — many real entries omit the field, and the re…

    #446
  65. May 2026

    Expand lookup to all 18 SRD categories with smart autocomplete

    Discord `/dnd` and the `/dnd` web page now cover **all 18** dnd5eapi.co 5e SRD categories (was 4) — adds monsters, classes, races, equipment, traits, ability scores, skills, and the rest. The Discord command's `query` option is now a **live autocomplete** that hits `/api/{type}?name={input}` as the user types, so they pick from a dropdown instead of guessing slugs. The web page gets an equivalent **custom dropdown** with arrow-key nav, debouncing, and `AbortController` so…

    #444
  66. May 2026

    Premium spoke wheel with mobile sizing + click-to-spin

    Three problems with the jackpot payout wheel (introduced in #442): **Mobile sizing.** `width: 320px; max-width: 90vw` with no responsive rules made the wheel dominate phone screens and visually crash into the stake input above and the banner description below. **"MS painty" visual.** Flat hex fills, a `▼` text-glyph pointer, a plain `<circle>` hub — looked nothing like the rest of the casino's `casino-felt` tables. **Auto-spin cutscene.** The wheel rotated the moment it ap…

    #445
  67. May 2026

    Subcommand rewrite + per-guild web UI

    Polishes the excuse service end-to-end: **`/excuse` rewrite** to real Discord subcommands: `random`, `submit`, `list`, `search`, `approve`, `delete`. Replaces the previous free-form `action` option that silently fell through to "create" when no known action was matched. **Per-guild web UI** at `/excuses` — the "excuse maker and finder": Finder card with a 🎲 Spin button (AJAX) and search box. Approved / Pending tabs (pending superuser-only). Per-row Approve/Delete buttons;…

    #443
  68. May 2026

    Tiered payout wheel replaces fixed-pct payout

    A "compromise" jackpot mechanic — high win chance but small slices most of the time, rare jackpots for the big hit. **Backend**: `JackpotHelper.rollOnWin` returns a `JackpotRoll(amount, tierIndex, tierPayoutPct)`. On a successful trigger it spins the per-guild `JackpotWheel` for a tier and credits that fraction of the pool. **Default wheel** `80:1, 10:5, 5:10, 4:20, 1:50` — 80% of jackpot wins pay 1% of the pool (pity), 1% pay 50% (mega). EV per win ≈ 3.1% — well below the…

    #442
  69. May 2026

    Replace campaign features with lookup page

    DnD Beyond now ships an official Discord bot that does campaign management, initiative tracking, encounter building and character-sheet integration better than TobyBot can. This strips all of that and replaces the `/dnd/campaign` web page with a small `/dnd` lookups page that exposes the same SRD search the existing `/dnd` Discord command already uses. ### Removed Discord-bot commands: `/campaign`, `/initiative`, `/character`, `/linkcharacter`, `/refreshcharacter` Initiati…

    #441
  70. May 2026

    Structurally exclude HighLow wins from rolling

    The per-guild `JACKPOT_RTP_MAX_PCT` gate uses RTP as a proxy for "deserves a jackpot sweetener". That proxy correctly blocks Coinflip/Blackjack/Baccarat/Roulette/Holdem at the recommended ceiling of 95, but it **breaks for HighLow**: an honest 12/13 ≈ 0.923 RTP, yet the player can pick direction against an anchor dealt from 2..12 and win ~85% of plays at the extremes (anchor=12 LOWER, anchor=2 HIGHER). `JackpotHelper.rollOnWin` fires **per winning play**, not per credit of…

    #440
  71. May 2026

    Preserve action-row buttons on refresh for weighted mode

    Companion fix to #438. After that PR the **mode label** on refreshed weighted-draw embeds was right, but the **action-row buttons** still disappeared. Same root cause, second site: `refreshAnnouncement` at line 166 was passing `lottery.mode` (the DTO column value `"TICKET_WEIGHTED"`) to `announcementActionRow`, which compares against `LotteryHelper.MODE_WEIGHTED` (`"WEIGHTED"`). The comparison missed, fell through to the URL-button else branch, and that branch returned `nu…

    #439
  72. May 2026

    Use runtime mode constant when refreshing announcement embed

    The lottery refresh job has been overwriting weighted-mode announcement embeds with the wrong "Today's draw" mode label (`Pick 5 of 49`) while the footer + yesterday's recap correctly say it's a weighted draw. Root cause: `rebuildWithUpdatedTodaysDraw` passes `lottery.mode` (the DTO column value `"TICKET_WEIGHTED"`) straight to `renderOpenSummary`, which expects the runtime config string from `LotteryHelper.MODE_WEIGHTED` (`"WEIGHTED"`). The comparison falls through to the…

    #438
  73. May 2026

    Make testcontainers work on Windows with Docker Engine 29

    Upgrade testcontainers to 1.21.1 and set docker-java API version to 1.44 for Docker Engine 29 compatibility Override `DOCKER_HOST` on Windows to use Docker Desktop's named pipe (`npipe:////./pipe/dockerDesktopLinuxEngine`) Share a single PostgreSQL container across Spring contexts via a static companion object, reducing 3 containers down to 1 Make `data.sql` idempotent with `ON CONFLICT DO NOTHING` to support container reuse Fix two `IntroWebServiceTest` failures by stubbi…

    #437
  74. May 2026

    Interactive buy button on announcement embed + modal infrastructure

    **Buy Tickets button** on weighted lottery announcement embeds — opens a modal for ticket count, processes the purchase via `buyTickets` service, replies ephemeral **Pick Numbers link button** on number match lottery embeds — opens the web lottery page (`/casino/{guildId}/lottery`), gracefully hidden when `APP_BASE_URL` is not set **Buttons persist across refresh edits** — `refreshAnnouncement` re-adds the action row via `setComponents` so buttons survive the 5-minute pool…

    #436
  75. May 2026

    Live-refresh announcement embed + thorough Command-helper rollout

    Two independent changes, one per commit. ### 1. Lottery announcement embed periodically refreshes when the pool grows The daily-lottery announce embed posts a "Today's draw — N credits in the pool" line, but post-announce ticket sales silently grew the pool with no visible feedback. Players had to run `/lottery status` to see the live number. `V29__lottery_announcement_message.sql` adds three nullable columns to `toby_coin_jackpot_lottery` (`announcement_channel_id`, `anno…

    #434
  76. May 2026

    Wide-ping + clickable buy CTA on daily announcement, rich top-holders on web

    Two improvements to the daily lottery so non-winners actually notice it and can act on it. ### Bot side — louder, clickable announcement The daily lottery embed used to post silently and only ping winners — anyone who hadn't bought a ticket had no signal that a new draw had opened. Now the announcement carries: **A configurable wide ping** — new `LOTTERY_PING_MODE` config (off / here / everyone, default everyone). Surfaced on `moderation/lottery.html` next to the announce-…

    #433
  77. May 2026

    Batch changelog entries into one PR (main is protected)

    The changelog automation added in #426 has been silently failing on every merged PR since it landed — #427, #428, #429 all triggered it, and every run failed in ~5 seconds at the final `git push origin main` step. No `chore(changelog)` commit has ever landed. **Root cause:** `main` is branch-protected, so the default `GITHUB_TOKEN` can never push there. The previous design's "direct-push to main with `[skip ci]`" was rejected by branch protection on every run. The prepend-…

    #431
  78. May 2026

    Moderation page split + searchable settings

    The single moderation page became six focused sub-pages (Users, Settings, Voice, Poll, Casino, Lottery). The Settings page got a sticky search box and a dedicated Jackpot section exposing five eligibility/payout configs that were previously backend-only. A new CI guard fails the build if a future config key is added without a UI row.

    #424
  79. May 2026

    Jackpot RTP eligibility gate

    High-RTP games (blackjack, coinflip, baccarat, roulette) can now be excluded from jackpot rolls via a configurable RTP ceiling, so they don't double-dip on a sweetener they don't need.

    #422
  80. May 2026

    Anti-autoclicker channel logs

    Suspected-bot session embeds now post to a configurable Discord channel and update in place as forced-loss substitutions accumulate over the session.

    #420
  81. April 2026

    Daily lottery (Pick 5 of 49 + weighted mode)

    Auto-runs at 00:00 UTC: closes the prior draw, pays tier-based prizes, opens a fresh one seeded from the jackpot pool. NUMBER_MATCH for high-engagement servers, WEIGHTED for low-traffic ones; configurable per guild from the moderation Lottery sub-page.

    #412
  82. April 2026

    Casino refresh: Roulette + Casino Hold'em

    Two new minigames join the quick-play roster, both wired into the per-guild jackpot pool with the same loss-tribute and stake-anchor rules as the rest.

    #404
  83. April 2026

    Multiplayer Blackjack tables

    Up to 7 seats per multi-table, configurable shot clock, S17/H17 dealer rule, and 3:2 / 6:5 payout toggle. Solo blackjack shares the same stake bounds.

  84. March 2026

    Poker tables (fixed-limit Hold'em)

    2-9 seats, configurable blinds and bet structure, per-actor shot clock with auto-fold, and rake routed to the jackpot pool.

  85. March 2026

    TOBY coin live market

    Per-server live market with a 5-minute price tick. Earn social credit by being active, then trade it for TOBY coin.

  86. February 2026

    Web moderation admin

    Browser-based moderation: toggle user permissions, adjust social credit, mute or move voice channels, post polls — no slash commands needed.