Moves the game-creation wizard state machine, view builder, and
platform-neutral contracts (callback data, step names, storage
exception, club option, step limits) from GmRelay.Bot to GmRelay.Shared.
Telegram continues to work through a new TelegramWizardMessenger
implementing IWizardMessenger and a WizardInteractionMapper that
converts Update → WizardInteraction. Wires the new platform column on
wizard_drafts (V032 migration) and switches chat/owner/thread/message
ids to TEXT so the same table can hold Discord snowflakes later.
- GameCreationWizard: now in Shared, takes IWizardMessenger +
IWizardDraftRepository, dispatches on WizardInteraction.
- New IWizardMessenger contract with Edit/Send/Answer/GetOwnerClubs
(returns string ids so Telegram longs and Discord snowflakes both
fit).
- New WizardStepViewBuilder in Shared returns
(text, IReadOnlyList<WizardAction>); TelegramWizardMessenger
renders actions into InlineKeyboardMarkup via a new Bot-side
ToInlineKeyboard helper.
- New WizardInteractionMapper in Bot (5-case test) converts Telegram
Update to WizardInteraction.
- WizardDraft gains a Platform column; ChatId/MessageThreadId/OwnerId/
DraftMessageId switched to string. V032 migrates existing rows and
rebuilds the owner lookup index on (platform, owner_id).
- All existing wizard / create-session tests updated to the new
contract (HandleInteractionAsync + WizardInteraction). Wizard
callback-data format preserved.
- dotnet build clean, dotnet format --verify-no-changes clean, all
101 wizard tests pass.
- Extract IWizardDraftRepository interface for testability (NSubstitute cannot
mock sealed classes; the codebase uses fake-style doubles instead).
- Add step-transition, pool-slot, validation, cancel/back, and render-shape tests
using FakeWizardDraftRepository and FakeWizardMessenger.
- Fix wizard payload persistence bug: HandleCallbackAsync and HandleTextAsync
now call SavePayload after ApplyChoice/ApplyText mutations, so subsequent
LoadPayload calls see the user's progress. Previously, local WizardPayload
mutations were discarded and the wizard reset on every step.
- CommitCurrentPoolSlot now auto-creates a slot via EnsureCurrentPoolSlot when
one is missing, so the PoolSlotCapacity → waitlist click is recoverable
even if the user lands on the step without a slot.
Dapper.AOT generated a 19-parameter ctor for ShowcaseSessionRow based on the
SELECT list in GetShowcaseSessionsAsync / GetShowcaseSessionAsync. After
adding PublicationMode and IsMembersOnly to ShowcaseSessionDto in v3.7.0 the
record itself was extended, but the SELECT still returned 19 columns, so the
materializer threw "A parameterless default constructor or one matching
signature (...) is required" and every request to /showcase returned 500.
Add s.publication_mode and (s.publication_mode = 'ClubOnly') to both SELECT
lists and propagate them through the ShowcaseSessionDto construction. The
field list now matches the generated constructor exactly.
Version bump 3.7.0 -> 3.7.1 (patch).
Implements Issue #110: game masters can now publish sessions
exclusively to a club's private showcase, gated behind a
member application and approval flow. Adds a 4-state
publication_mode (None/Catalog/ClubOnly/Both) replacing the
binary is_public, plus a club_memberships table with
Pending/Active/Rejected/Left lifecycle and partial unique
index ensuring a single Active row per (group, player).
Highlights
- V030 migration: club_memberships, publication_mode, drop
is_public, recreate partial indexes, portfolio_games gains
publication_mode.
- PublicationMode enum + extensions in GmRelay.Shared.
- ISessionStore gains 12 membership/showcase methods;
AuthorizedMembershipService owns the membership flow with
GM-only approve/reject authorization.
- PublicClub / PublicMasterProfile / PublicSession: member-
aware queries (ClubOnly visible only to Active members).
- New pages: MyClubMemberships (/profile/memberships) and
ClubApplications (/group/{id}/applications).
- GroupDetails and EditSession switch from a bool toggle to
a 4-state publication_mode selector.
- NavMenu adds Moji kluby, PublicLayout adds Kluby.
Tests: 4 new test files (PublicationMode, ClubMemberships,
AuthorizedMembershipService, ClubShowcaseSource) + updates
to PublicClubPages, AuthorizedSessionService/Portfolio
service FakeSessionStore, CampaignTemplatesNavigation.
493 tests pass.
Bump version 3.6.0 -> 3.7.0 across Directory.Build.props,
compose.yaml, deploy.yml, NavMenu.razor.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- PublicSession.razor: добавлена обработка ?register=1, AuthStateProvider,
TryGetPlatformIdentity, кнопки записи для авторизованных/неавторизованных
пользователей, отображение результата регистрации
- CreateSessionHandler: добавлен cover_image_url в INSERT SQL
- DiscordProjectStructureTests: версия 3.4.0 во всех проверках
- README.md: актуальная версия v3.4.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add publication settings for clubs and sessions, read-only public club/session pages, dashboard controls, privacy-focused public queries, docs, and tests.
Bump version to 3.3.0
Create a Telegram forum topic when Web creates a batch from a campaign template, persist thread ownership on the generated sessions, and send the batch schedule into that topic.
Bump version -> 3.1.1
- Добавлены миграции V024 (backfill + deprecation comments + calendar_subscriptions platform identity) и V025 (backfill proposed_by_external_user_id)
- Все Bot handlers переведены с telegram_id/chat_id на platform + external_*
- Shared handlers очищены от COALESCE fallback с telegram_* колонками
- DiscordBot очищен от COALESCE fallback
- Web SessionService и CalendarSubscriptionService переведены на external_*
- HandleRsvpHandler: убран legacy UNION с gm_telegram_id, теперь только group_managers
- RescheduleVotingFinalizer: переведен на external_username/external_user_id
- Tests: добавлены asserts для V024/V025
- Версия обновлена до 3.1.0
Bump version → 3.1.0
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- SessionSchedulerService now backs off for 15 minutes after any
handler failure (confirmation, one-hour reminder, join link),
preventing infinite retry loops on Discord 403 Missing Access.
- Added per-session ConcurrentDictionary backoff tracking with
automatic cleanup on success.
- Enhanced DiscordPlatformMessenger logging for SendConfirmation
and SendJoinLink to aid permission diagnostics.
- Added 3 regression tests for backoff behavior.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
DiscordNewSessionHandler.ParseTimeInput used DateTimeStyles.AssumeUniversal,
which interpreted user input as UTC. A user entering 15:00 got a session
scheduled at 18:00 MSK after rendering. Align with Telegram behavior by
treating input as Moscow time and converting to UTC before storage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
V023 migration drops NOT NULL constraints on:
- game_groups.telegram_chat_id
- game_groups.gm_telegram_id
- players.telegram_id
This allows Discord (and future platforms) to create players and
game_groups without legacy Telegram identifiers.
Bump version → 3.0.10
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>