Changelog
Curated highlights of what’s shipped recently. The full history lives on GitHub.
-
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 -
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 -
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 -
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 -
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 -
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 -
/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 -
Nav drawer stacking, additive embed rebuild, top holders include bonus tickets
## Three bugs reported after #509
#510 -
Tab-nav layout + silent web bonus award + broken bulk-buy hint
## Three bugs reported after PR #508 merged
#509 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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 -
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.
-
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.
-
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.
-
Web moderation admin
Browser-based moderation: toggle user permissions, adjust social credit, mute or move voice channels, post polls — no slash commands needed.