* [VDS] Drive folder and preview widgets from the model (MVC views)
Took 16 minutes
Took 8 minutes
Took 3 minutes
Took 11 minutes
* Rebase whoopsie
Took 4 minutes
* Hide widgets instead of destroying, go back to signals, rename for consistency.
Took 13 minutes
Took 4 seconds
Took 26 minutes
Took 5 seconds
# Commit time for manual adjustment:
# Took 9 minutes
* Make VDS startup smooth: batch deck loads, guard preview resizes
- Move color identity computation into the background load task and apply
finished deck loads in bounded batches per event loop turn, so finishing
hundreds of loads at once cannot stall the UI thread
- Skip redundant resize work in DeckPreviewWidget when the banner width did
not change, and collect the clamped children once instead of searching the
widget tree on every layout pass
Took 19 minutes
# Commit time for manual adjustment:
# Took 3 minutes
* [VDS] Expose filter matches as a proxy role instead of dropping rows
The folder display scanned source-model rows and probed acceptance with
mapFromSource(...).isValid(), reaching into both models for one answer.
The proxy now keeps every row and exposes each row's search/tag/color
filter result through FilterMatchRole. The folder display and the tag
filter read everything off proxy indexes, and hidden previews keep their
sorted position in the flow layout instead of being appended at the end.
Took 11 minutes
* [VDS] Bound pending-load drain by time and make row lookups O(1)
The fixed DECK_LOADS_PER_TURN = 24 cap had no measured basis. It was guessed and existed
because every applied load emitted dataChanged into each DeckPreviewWidget,
whose handler resolved its own row with an O(n) linear scan per widget.
The model now maintains a file path -> row hash kept in sync across scans,
renames and deletions, so rowForFilePath is O(1) and the fan-out cost is
gone at its source. The drain applies finished loads until a small time
budget per event loop turn runs out, so throughput self-tunes instead of
relying on an arbitrary count.
* Actual minimal fix for resize squishing
Took 20 minutes
* Fix color widget sizing
Took 16 minutes
* [BannerWidget] Also set a max height
Took 4 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Pin user list header length to the viewport width
The header stretch mode kept a resize section property, so after any
column grew past the viewport the list carried an invisible horizontal
pan range that scrolled rows sideways without visual feedback
Drop the leftover property so displayed length always equals viewport
width and horizontal panning is impossible
* Show columns 1 and 2
Took 12 minutes
Took 2 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [App] Add onboarding wizard
Took 10 minutes
Took 3 minutes
Took 7 minutes
Took 9 minutes
Took 2 minutes
Took 1 minute
Took 7 minutes
* Adjust CI
Took 14 minutes
Took 56 seconds
Took 2 seconds
Took 3 seconds
* Adjust CI again
Took 14 minutes
Took 2 seconds
* Comments and fixes
Took 9 seconds
Took 1 minute
* Rebase.
Took 5 minutes
Took 50 seconds
Took 15 seconds
* Comments.
Took 7 minutes
* CI lol
Took 3 minutes
* CI again lol
Took 4 minutes
* Drop some settings, add some new ones.
Took 19 minutes
* Resize when expanding section
Took 4 minutes
Took 3 minutes
Took 7 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [VDS] Cache mana symbol renders and skip redundant resizes
- Render each mana symbol once at a bounded master size and derive every
requested size from the cached master, avoiding repeated full-size SVG
rasterization on the GUI thread
- Share scaled results through a process-wide cache keyed by symbol and
size, so repeated widget creation and rescales don't redo the work
- Skip redundant resize work in ColorIdentityWidget and ManaSymbolWidget
when sizes did not change
Took 8 minutes
Took 50 seconds
* Move to pixmap generator
Took 8 minutes
Took 4 seconds
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [DeckList] Make deck tree card traversal recursive
getCardNodes and forEachCard now descend into nested zones instead of
assuming a flat main/side/token layout. For today's flat trees this is
behavior-preserving; it also removes two latent crashes (unchecked
dynamic_cast dereference, null card nodes passed to forEachCard
callers). Nested zones are introduced by later custom-zones units.
Took 3 minutes
Took 7 seconds
Took 9 seconds
* Fix rebase mistake.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Replace playmat crop spinboxes with a direct manipulation preview
The numeric fields exposed raw parameters with no relation to what the
game draws, accepted values the renderer clamps away, and no way to see
the result before committing
The preview now renders through the exact game pipeline into a viewport
shaped like a fresh board stack plus table area, with dimmed strips
marking where a developed table crops further. Drag pans, wheel zooms,
arrow keys nudge, plus and minus zoom, Backspace or Esc restores the
crop as of focus gain and lets Esc close the dialog when unchanged.
Focus ring and accessible name and description cover keyboard and screen
reader users, new paints use palette roles so themes recolor them
Took 25 minutes
Took 3 minutes
Took 54 seconds
* Remove stale constant
Took 3 minutes
Took 26 seconds
* Rebase
Took 2 minutes
Took 3 seconds
* Add editor spinboxes again
Took 4 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Introduce DECK_ZONE_MAYBEBOARD and its visible name, and treat the
maybeboard as editor-only scratch space: plain-text export, DeckStats
and TappedOut uploads now skip cards living there. Zones of this name
are created by later custom-zones units; until then the skips are inert.
Took 20 minutes
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
The offset and zoom fields exposed raw numbers with no relation to the
strip, and the stored printing reset to the first local printing on
every open
The preview now paints through UserListPainter::drawCardArt itself, so
what you see is exactly what the user list renders. Drag pans the art
vertically at output scale, wheel zooms, arrow keys nudge, plus and
minus zoom, Backspace or Esc restores the crop as of focus gain and lets
Esc close the dialog when unchanged. Margins stay explicit spinboxes
since they trim the strip sides with no natural drag mapping. Legacy
stored zoom below the gesture floor is normalized once on open, focus
ring and accessible name and description cover keyboard and screen
reader users, all strings set in retranslateUi
Took 7 minutes
# Commit time for manual adjustment:
# Took 55 seconds
Took 41 seconds
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
cn.svg and ss.svg defined their star polygons with coordinates around
plus/minus 5e5 compensated by tiny scale transforms. Qt drops shapes whose
device space bounds exceed its rasterizer coordinate limit, so both flags
silently lost their stars when rendered wider than roughly 76px. That
threshold was always exceeded because loadSvg with expandOnly renders at
the declared 640x480 native size before scaling down to the icon size.
Fold the scale transforms into the polygon coordinates so the geometry is
unchanged while bounds stay small at every render size.
Also cap expandOnly and usericon render canvases at four times the
requested size to bound memory use and keep pathological theme svgs away
from the rasterizer limit.
Took 8 minutes
Took 57 seconds
Took 3 minutes
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server] Add match result strategy hook
Took 7 minutes
Took 18 minutes
* Rebase.
Took 2 minutes
Took 13 seconds
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Two gaps made opening and confirming the banner dialog silently switch
the banner to the default art of the first local printing. The caller
dropped the card provider id when constructing the initial params, and
the constructor left the printing combo wherever onCardNameChanged put
it, which is always the first printing
Pass the provider id through, restore it in the combo when it resolves
locally, and keep it verbatim when it does not
Took 2 minutes
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Derive playmat sampling window from shared clamped helpers
The crop formula computed the sampled window inline with an unclamped
zoom floor, so stored vertical offsets below half of travel were dead
and extreme zooms could sample outside the art
Remap verticalOffset to place the window top edge within its travel,
floor zoom at visible width over min card side with a 4.0 ceiling,
clamp pan along the margin sum constant segment, and expose
playmatClampedZoom, playmatWindowSide and aspectFitRect so the game
renderer and any editor share one geometry model
Zoom 1 rendering is bit identical to before
Took 7 seconds
* Comments.
Took 29 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* native builds + merge
* naming and ordering
* use ninja and cmake build
* add ccache and cache mounts
* formatting
* Update servatrice.cpp
* Revert "Update servatrice.cpp"
This reverts commit 3acc684135.
* remove ccache again
cache mounts are not part of GHA caches from docker action
* comments and cleanup
Use buildx provided in runner, see https://github.com/actions/runner-images/blob/main/images/ubuntu-slim/ubuntu-slim-Readme.md
* more explicit
* comments, first pass
* ${{ runner.temp }}
* $(printf "$GHCR_IMAGE@sha256:%s " *)
* follow docker docs for latest and extract short semver from our tags
* not so pretty, but allows the easy inspect at the end
* add Servatrice name
* add links to runner images
* comments, second pass
* cleanup
Cached card art pixmaps carry the screen device pixel ratio, so both
banner painters did their crop math on scaled pixels and blitted the
result at raw over logical size, clipping art into its top left quadrant
on any display above 100 percent
Normalize a local copy to DPR 1 before crop math in UserListPainter and
the popup header, clamp srcX and srcY bounds against stored zoom below 1,
keep shared cache entries untouched
Took 15 minutes
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Show live connection latency in the status bar
Add a permanent status-bar label fed by ConnectionController's
pingStatsUpdated: shows the latest round-trip time, hides while
disconnected or without samples, and carries a tooltip with
Last/Median/95th percentile/Maximum over the rolling sample window
(mirrored into the accessible description). The server tab gets the
same stats as its tooltip.
Took 2 minutes
Took 2 minutes
Took 1 minute
Took 48 seconds
Took 25 seconds
Took 4 minutes
* [Client] Graph connection latency history in the status bar
Add LatencyGraphWidget, a size-agnostic bar sparkline over the rolling
sample window: heights scale to the window's own range while colors map
onto an absolute quality ramp, so a steady good ping stays green. Embed
it in the new LatencyStatusWidget together with the textual ping
readout and feed both through ConnectionController's forwarded signals;
the whole area hides while disconnected or without samples.
Took 9 minutes
Took 14 seconds
* [Client] Show latency details when clicking the ping display
Clicking the status bar ping area opens a popup with a larger instance
of the latency graph plus the numeric statistics, selectable and
mirrored into the accessible name. Qt::Popup closes it on any outside
click; contents refresh live while open.
Took 33 seconds
* Fixup from core commit
Took 6 minutes
Took 5 seconds
Took 5 minutes
* Lint.
Took 12 minutes
* Consolidate.
Took 6 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Detect main-thread event loop stalls
LagMonitor ticks the GUI event loop every 500 ms and records gaps
beyond 2 s as stalls, warning with their duration and keeping a
bounded ring of recent records for diagnostics. Measurement uses a
monotonic QElapsedTimer so wall-clock steps and suspend do not
fabricate stalls. Recorded timestamps stay in wall time for
correlating with user reports.
Took 1 minute
Took 13 minutes
Took 2 minutes
* [Client] Rename LagMonitor constants to SCREAMING_SNAKE_CASE
Took 15 minutes
* [Client] Discard suspend-spanning gaps in LagMonitor
Windows counts sleep time in its monotonic clock, so a suspend would
fabricate one bogus stall per resume. Reset the clock on application
state changes and drop implausibly huge gaps; extract recordGap() for
testability.
Took 3 minutes
* [Client] Unit test LagMonitor stall recording
Drives recordGap() directly to cover the threshold, plausibility cap,
trim, and clear behavior without timing-dependent waits.
Took 36 seconds
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Network] Measure real server round-trip times
Time each command container from send to response with QElapsedTimer,
aggregate samples in a fixed-size ring buffer (last/median/p95/max),
and emit aggregated pingStatsUpdated at most once per second so the
hot path stays free of signal traffic. Stats are cleared on
disconnect. Forward the signal through ConnectionController for UI
consumers. Unit-tested in latency_tracker_test.
Took 37 minutes
Took 4 minutes
# Commit time for manual adjustment:
# Took 8 minutes
* Move params to struct, more informative debug
Took 56 seconds
Took 53 seconds
Took 2 minutes
Took 33 seconds
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Server/Client/Protocol] Reporting users + moderation queue functionality
Took 6 minutes
Took 3 minutes
Took 8 seconds
Took 11 minutes
Took 12 minutes
Took 7 minutes
Took 15 seconds
Took 2 minutes
Took 1 minute
Took 30 seconds
Took 16 seconds
* CI Fix
Took 6 minutes
* CI Fix
Took 6 minutes
* [Protocol] Add moderation investigation commands
Adds the protocol layer for the moderation investigation suite:
- Command_GetUserSessions/GetUserAlts/GetModeratorLastLogins/ResetUserPassword/RemoveUserAvatar (1013-1017)
- Response extensions 1215-1219 with ServerInfo messages for sessions, alts, and staff logins
- last_login on Response_ReportUserInfo and warning_il on Response_WarnList
Took 2 minutes
* [Utility] Add warning categories parser with infraction levels
Parses the server's 'officialwarnings' setting (comma-separated, optional
'|IL' suffix) into WarningCategory structs so the client can display the
infraction level of each warning category. Includes GTest coverage.
* [Server] Add moderation investigation tools
Implements the server side of the moderation suite:
- getUserSessions/getUserAlts/getModeratorLastLogins/removeUserAvatar DB methods
- Handlers for all five new commands with audit records (PASSWORD_RESET,
REMOVE_USER_AVATAR); password resets return a generated temporary password
- cmdGetWarnList now reports per-category infraction levels from the
officialwarnings setting; cmdReportUserInfo reports last_login
- Update servatrice.ini.example with the warning taxonomy
- Password/avatar mutations report RespNameNotFound when the user does not exist
* [Client] Add moderation tab with investigate, password reset, and avatar removal
- New Moderation tab: search a user to show account info, alternate
accounts, login sessions, and staff last logins; actions to reset the
user's password (shows the generated temporary password) and remove the
user's avatar
- 'Investigate user' entry in the user context menu opens the tab pre-loaded
for that user
- Warning dialog shows the infraction level of each warning category
- Tab wired into TabSupervisor with a moderator-gated menu action, shortcut,
and tabs.ini persistence (default closed)
* [Server/Client/Protocol] Address PR #7091 review: security, bug, and perf fixes
Security:
- Promote RESET_USER_PASSWORD to admin-only dispatch (was moderator-accessible)
- Reject password reset on users with equal/higher privilege than caller
- Notify affected user via Event_NotifyUser::CUSTOM when password is reset
- Add server-side category whitelist for reports
- Drop reporter name fallback in comment/details authorization (ID-only)
- Force password change: new DB column + login enforcement + client disconnect
Bugs:
- XSS via QTextEdit::append() → insertPlainText() in report tab and utils
- allNotified initialized to true even with empty recipients list
- Warning combo box: use currentData() instead of baked-in display text
- Report resolution now records who resolved (resolved_by column + audit)
Performance:
- IP-correlation subquery: add 6-month window + LIMIT 200
- getUserSessions: clamp limit to 500
Non-blocking:
- Palette-aware colors in report_utils.cpp (dark/light mode)
- Report list pagination: offset/limit fields + total_count in response
- SessionCommand enum gap comment for reserved values 1201-1203
Schema: 36→37 (force_password_change), 37→38 (resolved_by)
Took 12 minutes
Took 16 seconds
* Fix macOs pedantry
Took 5 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Game] Playmats
Took 19 seconds
Took 1 minute
* [Playmats] Add fixed override and configurable fallbacks to settings.
Took 29 minutes
Took 43 seconds
* Add main to test.
Took 1 minute
Took 29 seconds
* Move settings to own group
Took 11 minutes
* Some attempts to refresh macOS compositor
Took 2 minutes
* Try something else
Took 17 minutes
* Don't manipulate live list
Took 11 minutes
* Change things about resolution, address comments.
Took 45 minutes
Took 12 minutes
* Comments.
Took 14 minutes
Took 8 seconds
* Re-order settings menu location
Took 2 minutes
* Rename PlaymatResolution to Info and add enums
Took 8 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Cards] Artist attribution
Took 7 minutes
Took 4 minutes
* Nudge attribution pill on home screen to align
Took 3 minutes
Took 28 seconds
Took 38 seconds
* Lint.
Took 3 minutes
* Lint.
Took 2 minutes
* Fix rebase whoopsie
Took 5 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Send game invites from the user context menu via a private message
The user context menu gains an "Invite to Game" submenu listing the
inviteable games in the room (the inviter's own games, honoring the
buddy-only setting). Picking one opens a private message to the target
user with a cockatrice://joingame link naming the game, so the target
gets a clickable invite instead of a raw URL. Multi-game rooms offer a
picker; a single inviteable game sends directly. Sending a message to
an offline user no longer swallows the draft — it reports that the
user is offline and keeps the typed text.
Took 1 minute
* [Client] Add invite-to-game dialog to the game window
Took 15 seconds
* [Client] Open the invite dialog taller by default without enforcing a minimum size
* Move button to bottom
Took 3 minutes
* Address comments.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Send game invites from the user context menu via a private message
The user context menu gains an "Invite to Game" submenu listing the
inviteable games in the room (the inviter's own games, honoring the
buddy-only setting). Picking one opens a private message to the target
user with a cockatrice://joingame link naming the game, so the target
gets a clickable invite instead of a raw URL. Multi-game rooms offer a
picker; a single inviteable game sends directly. Sending a message to
an offline user no longer swallows the draft — it reports that the
user is offline and keeps the typed text.
Took 50 seconds
Took 3 minutes
* [Client] Extract sendPrivateMessage() to fix invite message draft overwrite
sendInviteMessage() was calling sayEdit->setText(text) then sendMessage(),
which overwrites any text the user had typed. Extract the command-building
and sending logic into a new sendPrivateMessage(const QString &text) method
that takes the text directly. sendMessage() now calls it after its guards
and clears sayEdit; sendInviteMessage() calls it directly without touching
the input field at all.
Took 33 minutes
* Rename method, address comments.
Took 5 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Confirm before joining a game opened from a game link
Joining a game from a cockatrice://joingame link is a navigation
decision, so restate what will be joined and ask before acting:
the confirm names the game description when the link carries one
(falling back to the room name and numeric id for older links), and
reports the host:port so links that point at a different server are
obvious. The intent chain is only started after confirmation.
Took 3 minutes
* [Client] Extract the join-game confirm message into a helper
Took 3 minutes
# Commit time for manual adjustment:
# Took 6 seconds
* Proper fwd declare.
Took 3 minutes
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [UserList] Show amount of online buddies
Took 11 minutes
* [UserList] Replace early return with if-else in updateSectionDivider
RickyRister nit: the code is easier to follow with a standard if-else
branch instead of an early return for the Buddy section.
Took 1 minute
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Client] Route cockatrice:// link clicks from chat to the intent chain
A cockatrice:// link clicked in chat is currently handed to the OS (or
does nothing in-process). Clicks now emit a cockatriceLinkActivated signal
that travels ChatView -> Tab -> TabSupervisor -> MainWindow, which feeds
the URL through the same IntentUrlParser the OS activation path uses, so
the join runs entirely in-process. card/user schemes and all other links
behave as before.
* [Client] Route cockatrice:// link clicks from the in-game chat to the intent chain
* [Client] Reuse one IntentUrlParser instance for cockatrice:// links
Took 59 seconds
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
* [Chat] Render cockatrice://joingame links in chat as clickable buttons
Words starting with cockatrice:// become button-style anchors labelled with
the game description, id and server (falling back to id + server for links
built without a description). The description is spliced via the multi-arg
arg() overloads so a title containing "%…" cannot corrupt the label.
Keyboard link access is enabled so the anchors are reachable without a mouse.
* [Chat] Fix percent-encoding and scheme gating in game-link chat labels
Game descriptions containing '%' were rendered as '%25' in the chat
button label because QUrlQuery's default decode leaves %25 untouched.
Use QUrl::FullyDecoded for the description item, and restrict the
invite-button treatment to cockatrice://joingame links; any other
cockatrice:// scheme now falls through to plain text.
---------
Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>