Commit graph

1621 commits

Author SHA1 Message Date
wdunn001
62708af762 Merge branch 'develop' of forgejo:drop-oss/drop into develop
Some checks failed
Server CI / Lint (push) Failing after 16s
Server CI / Typecheck (push) Failing after 19s
2026-08-20 23:32:39 -04:00
wdunn001
5278bce000 feat(profile): user avatar upload endpoint, UserAvatar in header and profile, account home rework 2026-08-20 23:32:27 -04:00
wdunn001
927f9aae21 feat(game-accounts): add account recovery action, optional password on provision, settings page panel and design doc update 2026-08-20 23:32:26 -04:00
wdunn001
5516453b7f server: tell pre-0.4 clients they are outdated instead of a bare 403
Some checks failed
Server CI / Lint (push) Failing after 2m6s
Server CI / Typecheck (push) Successful in 4m2s
Old clients pair fine (the handshake is unauthenticated) then fail every authenticated call with a message-less 403, which the desktop app renders as an opaque 'Unrecoverable error'. Gate /client/auth/initiate on the User-Agent the 0.4+ remote crate always sends, and give the non-JWT auth fallthrough an explicit statusMessage/message pointing at cdn.quasarke.net/drop-client.
2026-08-14 12:21:57 -04:00
wdunn001
a74ed9eded feat(community): SSO to native game account bridge, phases 2 and 3
Some checks failed
Server CI / Lint (push) Failing after 2m0s
Server CI / Typecheck (push) Successful in 3m28s
Adds the player-facing half of game account management: a panel on
/community/servers/[id] that shows your linked account for that server,
creates one, and changes its password.

None of these emulators speak OIDC and none will - each ships its own
login against its own auth database. So this is a bridge, not SSO: one
Drop identity tied to one native account per server, enforced by a unique
index on (userId, serverSlug). That constraint IS the feature.

Drop owns identity, authorization, the link table and the UI. The
per-game DB access and password crypto stay in mmo-portal's provider API
(SRP6 verifiers, EQEmu hashing) - code that writes into live auth DBs and
fails silently when subtly wrong.

The password never touches Drop's database; it goes straight through to
the provider, which turns it into whatever that emulator wants.

Notable details:
- GameServer.slug names the game, provider.key names the emulator. They
  agree for eqemu and daoc but wow != trinitycore, so the mapping is an
  explicit three-entry constant, not an assumption.
- Panel renders NOTHING when a server has no account backend (Impostor is
  a lobby with no identity), rather than a dead button.
- 'backend unreachable' is kept distinct from 'you have no account' - the
  latter would invite a duplicate during an outage.
- Provision creates in the game first, then links. The reverse would
  leave a link pointing at a nonexistent account, which reads as
  corruption to every later call.
- Authorization is just an authenticated Drop session with the ACL: Drop
  access and game access already imply each other, so a third gate could
  only disagree with them.

Typecheck clean.
2026-08-12 17:04:33 -04:00
wdunn001
1e1f005f76 docs(design): correct the slug-to-provider-key mapping
Two of three match (eqemu, daoc) but wow does not - Drop names the game,
the provider names the emulator (trinitycore). Needs one explicit alias
constant, not an assumption that slug == key. Asserted in mmo-portal's
tests so a rename breaks a test rather than the mapping.
2026-08-07 18:16:05 -04:00
wdunn001
0572321b5d docs(design): record owner decisions on game account management
Reframes the feature as an identity bridge: these emulators do not and
will not speak OIDC, each ships its own login against its own auth DB, so
the value is tying one SSO identity to one native account per server.

That makes two former open questions answers rather than choices - one
account per user per server IS the feature, and no authorization gate
beyond a Drop session, since Drop access and game access already imply
each other and a third check could only disagree with them.

Retail TrinityCore on hold.
2026-08-07 16:39:00 -04:00
wdunn001
51a0e13a98 docs(design): scope game account management moving into Drop
Ports mmo-portal's cross-game provider layer, provisioning and admin
dashboard into Drop for all live servers. GameServer.slug is the join key
to provider.key - no new identifier.

Recommends keeping the Python provider layer as an internal service
rather than reimplementing SRP6 and EQEmu password hashing in TypeScript;
that code writes into live auth DBs and fails silently when wrong.
2026-08-07 16:16:55 -04:00
wdunn001
a5c1ed6524 docs(quasarke): VR player guide and operator reference
Some checks failed
Deploy website to GitHub Pages / build (push) Failing after 4m59s
Deploy website to GitHub Pages / deploy (push) Has been skipped
Player guide covers the two supported hardware paths (Lighthouse and Quest),
the SteamVR runtime install that needs no Steam account or client, and which
titles work, including the ones that do not: Half-Life Alyx and The Lab need
a real SteamVR backend, No Man's Sky VR is broken through the translation
layer, and VRChat/Rec Room/Zenith depend on their companies' servers.

Operator reference covers the architecture, the routing decision tree, why
per-process XR_RUNTIME_JSON is preferred over the machine-wide registry
switch, why OpenVR routing uses a per-game DLL swap, and the staging/import
ordering constraint.

Both state plainly that only runtime startup has been validated: tracking and
per-title rendering are unverified on real hardware.
2026-08-04 04:43:21 -04:00
wdunn001
f9ea2b87e1 docs: retire the browser-linkout Community docs, document native client auth
Some checks failed
Deploy website to GitHub Pages / build (push) Failing after 2m42s
Deploy website to GitHub Pages / deploy (push) Has been skipped
The Quasarke Edition docs still described the desktop client's Community
tab as either an upstream "Under construction" stub or something that
opens in the system browser. Neither is true anymore -- e8d3df03 retired
that entirely and replaced it with native News/Friends/Alerts/Chat pages
(0.4.4). News is confirmed working against the live server; the new
profile page and achievements card are built but not yet verified running,
so they're marked unverified rather than shipped.

Also:
- New client-auth.md documents the two credential types the desktop client
  uses (client JWT for /api/v1/client/*, opaque webtoken for
  aclManager-gated routes) and the CLIENT_WEBTOKEN_ACLS scopes -- this cost
  three rebuild cycles because the wrong one produces a clean 403 that
  looks like a permissions bug.
- Corrects the "EQEmu player count is genuinely unavailable" claim: WoW,
  OpenDAoC, and EQEmu all read real live player counts now (characters
  .online, serverstats with a 150s staleness cutoff, and Spire's
  authenticated API via a drop-service account, respectively).
- Notes the published client (0.4.1) is behind current source (0.4.4).
- Corrects Linux packaging: real .deb/.rpm (amd64) now exist; no AppImage
  (never a bundle target) and no arm64 -- and flags a leftover copy bug on
  the download page itself still advertising AppImage.
- Updates row counts (CommunityTitle, Mod, AchievementDefinition, game
  server registry) against a direct database check.
2026-08-04 01:13:13 -04:00
wdunn001
c7ffc2b994 desktop: route News through the client-JWT path; bump to 0.4.4
News called /api/v1/client/news through apiGet(), which sends the
webtoken Bearer obtained from POST /api/v1/client/user/webtoken. But
that route is a defineClientEventHandler -- it verifies the clients own
short-lived signed JWT and never consults the webtokens ACLs at all, so
it rejected the request outright. Store and Library have always worked
because they already use the JWT path.

Adds api_get_jwt on the Rust side and apiGetClient() in the composable,
and points News at it. Verified working against the live server.

Two auth mechanisms coexist here and picking the wrong one yields a
clean 403 that looks like a permissions problem: /api/v1/client/* wants
the client JWT, while routes gated by aclManager.getUserIdACL want the
webtoken plus the right ACL.
2026-08-03 23:46:04 -04:00
wdunn001
79aeaa9c24 desktop: bump to 0.4.3
0.4.2 was built two minutes before the webtoken fix landed, so that
artifact carries the broken JSON parse and cannot authenticate any
community API call. Bumping so the fixed build is unambiguous.
2026-08-03 20:36:06 -04:00
wdunn001
e33c2c1683 desktop: read the webtoken response as text, not JSON
The server route returns a bare string (return token.token), which h3
sends as a raw text/plain body rather than a JSON-quoted string. Parsing
it with .json::<String>() failed against the live server -- serde_json
read the UUID leading hex as a number literal and errored with "invalid
number at line 1 column 7".

Without this the client cannot obtain a web token at all, so every
community API call (news, friends, notifications) fails to authenticate
regardless of the page implementation.
2026-08-03 20:02:49 -04:00
wdunn001
e8d3df032c desktop: native News, Friends, and Alerts pages; retire the browser-linkout Community tab
Some checks failed
Server CI / Lint (push) Failing after 9m53s
Server CI / Typecheck (push) Successful in 11m40s
The News page was a hardcoded "under construction" stub, and the Friends/
Alerts header icons did nothing -- both replaced with real Vue pages/
widgets backed by the server's existing news/notifications/community APIs,
reached through a generic authenticated REST bridge (community_api.rs:
api_get/api_post/api_delete) and a websocket bridge for live chat/presence
(community_ws.rs, mirrors the reqwest_websocket pattern remote.rs already
uses for the auth-code exchange).

- News: GET /api/v1/client/news, rendered with micromark, with real
  loading/empty/error states.
- Alerts: GET/POST /api/v1/notifications/*, polled (not the notifications
  websocket -- see notifications.ts for why), unread badge + mark
  one/all read.
- Friends: full friends composable (list/requests/search/presence) plus a
  header dropdown and a /community/friends management page.
- Chat: /community/chat, rooms (server/global/title/DM) over the live
  community websocket with a REST history/read-state load on room switch.
- Achievements: per-game card on the library detail page and a per-user
  summary on the new read-only profile page (/community/profile/:username).
- The Community tab's "opens in your system browser" page is gone;
  /community is now a real in-app hub (activity feed + server list) linking
  into friends/chat.

Also fixes a real auth gap found by actually running the built client
against the live server: notifications/community routes are gated by
aclManager.getUserIdACL, which only accepts a session cookie or an opaque
Bearer APIToken -- never the client's own short-lived signed JWT
(generate_authorization_header). The client already had a bridge for this
(POST /api/v1/client/user/webtoken, JWT-authenticated, mints an opaque
token), but CLIENT_WEBTOKEN_ACLS never granted it the notifications/
community scopes, so the exchange succeeded and the minted token still got
403'd on every one of these routes. Extended that ACL list
(04.auth-init.ts) and switched community_api.rs/community_ws.rs to mint and
use that webtoken instead of the JWT for these specific calls.

Client version bumped to 0.4.2 (tauri.conf.json).
2026-08-03 19:42:50 -04:00
wdunn001
e1fa7e16c5 desktop: update Cargo.lock for the reqwest gzip/zstd feature bump 2026-08-03 15:34:12 -04:00
wdunn001
06fa4ae727 desktop: enable gzip+zstd reqwest features so chunk downloads honor ingress compression; bump client to 0.4.1
Cargo.toml previously built reqwest with default-features=false and no
compression feature, so the client never sent Accept-Encoding on chunk
GETs and never decoded a compressed response -- Caddy-side encode would
have been a silent no-op. zstd is listed first to match the measured
best cost/ratio tradeoff (zstd-3 ~0.70 ratio in 0.18s/64MB chunk vs
zstd-19's 10.8s); gzip stays as the fallback for any proxy hop that
only speaks gzip.
2026-08-03 15:22:13 -04:00
wdunn001
1c894a3685 docs(design): 0001 import-time chunk compression pipeline
Design proposal only. No behaviour changes; nothing here is compiled,
built or deployed.

Compress chunks once at version import into a derived, content-addressed
sidecar store, rather than compressing at serve time or as a separate
manual pass. Codec is zstd, declared as a versioned per-chunk property so
it can be replaced later without a migration.

Three structural decisions carry the design:

- The compress/skip decision is per chunk, not per version. Measured
  zstd-19 ratios across real 64MB samples span 0.057 (.cam) to 0.997
  (.bdt); content-type variance dwarfs codec choice by roughly 17x
  against 1.10x.
- Raw is not a stored representation, it is the absence of one. A chunk
  that does not clear the ratio threshold is simply not written, and is
  served by the existing offset-slicing path unchanged. Incompressible
  content therefore costs zero extra storage and zero new serve-path code.
- Compression is opt-in at the wire level via X-Drop-Accept-Codecs. A
  client that declares nothing gets exactly what it gets today, so old
  clients keep working with no change at all.

Also covers: the ManifestWriterFactory seam (already writes chunk bytes,
currently passed None by torrential, so compression adds no read I/O);
a distinct IV for the compressed representation to avoid AES-CTR
keystream reuse; bounded and niced encoder pool held apart from
READER_SEMAPHORE so .88 is not starved; failure paths that all degrade to
raw; and an opportunistic backfill that rides the existing 42-game
metadata reimport backlog instead of a 130 CPU-hour batch.

Deliberately not promising a headline savings figure: the measured 35%
mean is per-file, not byte-weighted, and large archives compress worst.
The pipeline reports the real byte-weighted ratio per import instead.

Open questions for review are in section 15.
2026-08-03 15:17:31 -04:00
wdunn001
ffc46233c3 Merge desktop client work into develop
The launcher community integration lived only on feat/quasarke-desktop-client:
community.rs, cloud_saves_commands.rs, the Saves tab, and the settings pages
were absent from develop, so the published installer carried features the
mainline did not and the next build from develop would have silently lost them.

The branch also carried an older parallel implementation of the server-side
community work (community.prisma, browseService, profileService,
titleSyncService, titles.get.ts, the ACL lists) which M2-M5 have since
rewritten. All 17 conflicts were in that server/docs/build surface and were
resolved in favour of develop; the merge therefore changes desktop/ only.
2026-08-03 13:35:56 -04:00
wdunn001
48d07a9da6 docs(sites/docs): mention the Community browser's Show-all UI toggle, not just the query param
Some checks failed
Deploy website to GitHub Pages / build (push) Failing after 9s
Deploy website to GitHub Pages / deploy (push) Has been skipped
2026-08-03 13:24:18 -04:00
wdunn001
e3e1cbc87f docs(sites/docs): bring Quasarke Edition section up to date with M2-M5 community launch
Some checks failed
Deploy website to GitHub Pages / build (push) Failing after 8s
Deploy website to GitHub Pages / deploy (push) Has been skipped
Community moved from "in development" to mostly-live: unified browser,
friends/presence, chat, game-server listings, mods, and achievements are
all real and running at drop.quasarke.net/community, not the old
community.quasarke.net sidecar. Rewrote the feature status table against
live DB counts and the running site rather than the design doc, and kept
the same honesty discipline on what's still not true: WoW/DAoC/EQEmu
player counts are unavailable pending a deploy, chat has never been
proven with two real users, the launch wrapper's backend is live but no
device has enrolled, achievement schemas only cover two titles, mods
downloads work but the CDN path in the catalog doesn't, and the desktop
client's own Community tab and Linux packages are still gaps, not shipped
features. Added a short note on keeping this section current going
forward.
2026-08-03 13:15:14 -04:00
wdunn001
3f89ddd11f community: fix Spire login path, add staleness cutoff for DAoC serverstats
Some checks failed
Server CI / Typecheck (push) Successful in 5m37s
Server CI / Lint (push) Failing after 7m17s
- Spire's auth controller is mounted at bare /auth, not /api/v1/login or
  /api/v1/auth/login (confirmed live via Spire's own unauthenticated
  GET /api/v1/routes introspection endpoint). Two earlier guesses both
  405'd. Default login_path corrected to /auth/login.
- mysql_direct gains value_column/observed_at_column/max_staleness_sec:
  OpenDAoC's serverstats is a periodic snapshot (written once a minute by
  the server's own StatSave.cs), not a live aggregate, so a query must read
  the newest row and discard it as unavailable once it's too old rather
  than presenting a stale reading -- possibly from a since-crashed server
  -- as a current live count.
- Registry updated for both now-confirmed-live pieces: daoc-mysql's
  serverproperty.statsave_interval is enabled (1 minute) and verified
  writing; the drop-service Spire account exists (user ID 3), credentials
  in OpenBao kv/eqemu/spire, materialized host-side into
  /opt/drop/spire-service.env (not yet mounted -- compose change is
  separate, see homelab-compose).
2026-08-03 12:55:53 -04:00
wdunn001
904f0f3351 community: replace SSH game-server probes with direct DB/API reads over mmo-net
Some checks failed
Server CI / Lint (push) Failing after 4m46s
Server CI / Typecheck (push) Successful in 6m10s
WoW/TrinityCore and OpenDAoC population is now read via a direct mysql2
connection to trinitycore-mysql/daoc-mysql over the mmo-net bridge (drop
joins that network in homelab-compose), the same shape Drop already uses
for RomM's catalog (rommAdapter.ts) and mmo-portal already uses for these
exact databases. Each query is copied from that game's own management
tooling rather than invented against the schema: WoW's matches FusionCMS's
Characters_model.php::getOnlineCount() verbatim; OpenDAoC's matches
upstream OpenDAoC-Core's own StatSave.cs/stats.php reading serverstats.

EQEmu has no DB-backed online-player source (re-verified live: peq has no
who_all_cache, no characters.online-equivalent, no count anywhere) --
corrects an earlier "genuinely unavailable" claim, which was wrong about
there being no source at all. The real source is World's in-memory client
list, exposed safely through Spire's own authenticated HTTP API
(GET /api/v1/eqemuserver/server-stats, backed by a telnet admin console
that must stay loopback-only). Added a spire_session_api probe method that
logs in as a dedicated service account and reuses the JWT; degrades to
"unavailable" honestly until that account is provisioned.

Removes the ssh_mysql_query and ssh_docker_logs_regex methods -- the
deployed image never had an ssh client, so both always failed. Does not
touch presence_inferred (Goldberg relay), which already degrades to
"unknown" without ssh, by design; not fixed here per explicit instruction
not to add openssh-client to the runtime image or probe the relay's game
ports.
2026-08-03 12:48:57 -04:00
wdunn001
0106a6bd34 community: game-server information provider contract (design + interfaces)
Some checks failed
Server CI / Lint (push) Failing after 11m32s
Server CI / Typecheck (push) Successful in 13m16s
One contract, many providers. Design document plus the TypeScript
interfaces; no implementation, no changes to the existing panel or
services.

Two structural rules, both aimed at bugs we actually hit:

- Population is a discriminated union, not `number | null`. A provider
  that declares population unsupported has no code path to a number, so
  the "0 online now" the panel showed for EverQuest (whose count is
  genuinely unobtainable: Spire needs an authenticated session, the PEQ
  schema has no online column) becomes unrepresentable rather than
  merely fixed.
- "Down" and "could not check" are separate outcomes. `unreachable`
  requires evidence; a transport failure yields `indeterminate`. The
  float ConnectTimeout bug that reported a live relay as DOWN would now
  produce "status unknown".

Providers declare capabilities (liveness, population, player list,
instances, realms, map/mode, latency, version, join info) and the panel
renders from those declarations, so a new game needs no panel edit.
Every fact carries provenance: measured, queried_side_channel, inferred,
or self_reported.

Four topologies modelled without flattening: persistent_world (EQEmu,
TrinityCore, OpenDAoC), instance_fleet (Quake, Counter-Strike),
lobby_host (Hawken, Impostor -- an instance_fleet whose members have no
own address), peer_mesh (the Goldberg relay). ProbeDiscipline carries
neverContactEndpoint, enforced by the runner rather than by a comment,
because gbe_fork registers any prober as a peer.

Survey findings folded in: we run zero Quake/CS servers today, gamedig
gives direct query of all those protocols for free, dpmaster is the only
credible self-hostable master and Source 2 / CS2 has none at all, so the
recommendation is a configured endpoint list plus direct query rather
than running a master. The relay's journald peer log is a readable,
zero-packet occupancy source we were not using.

Files are new and self-contained; migration is four additive phases
keeping the existing GameServer columns working throughout.
2026-08-03 11:06:21 -04:00
wdunn001
180241c266 community: wire server registry deep links + main nav (friends widget, Community submenu, profile link)
Some checks failed
Server CI / Lint (push) Has been cancelled
Server CI / Typecheck (push) Has been cancelled
- Resolve gameServerRegistry.json's null deep_link entries against the
  live drop-db catalog (WoW, EverQuest, Among Us match real Game rows;
  OpenDAoC and the Goldberg relay genuinely have none, left null and
  documented why).
- Fix the "0 online now" panel bug: the community landing page's
  persistent-world quick-strip was inferring presence from PlaySession
  name-matching (presenceService.getServerPresence), which hardcodes
  available=true even with zero matches -- always showing a fake "0"
  for EQEmu instead of the honest unavailable state. Switched it to the
  same probe-backed GameServer feed ServerStatusPanel already uses
  (joined by the shared wow/eqemu/daoc slug), so known-zero and
  count-unavailable never render the same.
- Header people icon now opens a friends flyout (list, incoming
  requests with accept/decline, entry into chat with a friend) instead
  of going nowhere, mirroring the existing notifications bell's
  Menu/MenuButton/MenuItems pattern; badge shows pending request count.
  Mobile sidebar links it straight to /community/friends.
- Community gets a hover submenu in the main nav (desktop) / inline
  sublist (mobile) listing Friends, Chat, Devices, Mods, Servers,
  Issues, Achievements.
- User dropdown menu gets a "Your profile" entry resolved from the
  signed-in user's own username (/community/profile/<username>), not
  hardcoded.
2026-08-03 11:02:58 -04:00
wdunn001
feb5f29190 community: fix presenceAvailable/playing type mismatch breaking the build
Some checks failed
Server CI / Typecheck (push) Successful in 3m8s
Server CI / Lint (push) Failing after 40m17s
getFriendsActivity's declared return type omitted presenceAvailable
entirely (both the early-return-on-no-friends branch and the type
annotation), and community/index.vue's local FriendActivityEntry.playing
was typed {gameId, name} when presenceService.ts's actual FriendPlaying
shape is {platform, id, name} -- the template only ever reads `.name`, so
only the type needed to change. This was breaking `nuxt build`'s typecheck
step on plain `develop` (confirmed via a clean checkout before any M4 code
was involved), which meant nothing downstream of M3's chat/presence
landing could build. Fixed forward rather than reverting anything.
2026-08-03 03:53:18 -04:00
wdunn001
7021af50fb community: M4 -- add GameServer.slug, regenerate migration against M2/M3/M5 baseline
Some checks failed
Server CI / Typecheck (push) Has been cancelled
Server CI / Lint (push) Has been cancelled
Stable slugs (wow/eqemu/daoc) match M3's chatService.ts KNOWN_SERVERS keys
exactly, so ChatRoom.gameServerId's reserved backfill can match on slug
equality later. Migration timestamp moved past M2/M3/M5's so it applies
last; regenerated via schema-to-schema diff (not `prisma migrate dev`) so it
only contains this milestone's delta, not a whitespace sweep of models this
milestone doesn't own.
2026-08-03 03:23:41 -04:00
wdunn001
0348d10602 community: M4 -- game-server registry and issue reports
GameServer/GameServerStatusHistory/Issue/IssueComment models, a
strategy-per-probe_type ServerProbe (tcp_connect/http_get/presence_inferred,
mirroring script-library's game-server-registry-probe.py including the
integer ConnectTimeout fix), the bundled verified registry seed (WoW, EQEmu,
the Goldberg relay, Impostor, OpenDAoC), and full player+admin UI at
/community/servers and /community/issues. Featured flag pins the
persistent-world servers (WoW/EQEmu/OpenDAoC) to a compact status panel
surfaced on the community landing page. Scheduled sweep respects a
circuit-breaker backoff and never probes on page load; the relay is never
network-probed, only liveness-checked over ssh.
2026-08-03 03:19:54 -04:00
wdunn001
dcf6507af5 community/chat: register a websocket error handler
Some checks failed
Server CI / Typecheck (push) Failing after 6m26s
Server CI / Lint (push) Failing after 6m32s
WebSocketHandler throws on the literal "unauthenticated" server response
when no .error() handler is registered, before its listen() callback ever
runs -- and since this composable connects at module scope (Nuxt bundles
auto-imported composables into a shared chunk), that happened on every
signed-out page, /auth/signin included. Found live: a browser check of
/auth/signin hung on an uncaught exception from this exact path.
2026-08-03 01:55:23 -04:00
wdunn001
e80033e68e community: M3 -- friends, presence, chat
Some checks failed
Server CI / Lint (push) Failing after 2m23s
Server CI / Typecheck (push) Failing after 2m56s
Friendship (request/accept/decline/remove, self-request and crossing-request
handling, DB-level pair uniqueness via a hand-written expression index),
ChatRoom/ChatMessage/ChatReadState backed by Nitro's built-in websocket
(server/api/v1/community/ws.get.ts) behind a ChatTransport seam
(server/internal/community/chatTransport.ts) with a REST send fallback.

Mid-milestone product redirect: chat's primary job is coordinating the
persistent-world servers (EQEmu/WoW/DAoC), not per-title discussion --
ChatRoomKind gets a first-class `server` case and chatService.ts ships a
static KNOWN_SERVERS registry keyed the same way M4's GameServer will be,
once that lands (gameServerId reserved for that wiring-up pass).

Presence has no new table -- "online" is a live websocket registry
(presenceRuntime.ts), "playing X" reads through an isolated query module
(presenceService.ts) that degrades to an empty, clearly-labeled result if
PlaySession's shape doesn't match what it expects.

Pages: /community/friends, /community/chat, plus a friends/servers strip on
/community itself. Migration 20260803120000_m3_friends_chat.
2026-08-03 01:39:04 -04:00
wdunn001
e668c41eef community/mods: fix intra-batch dependency resolution ordering
Some checks failed
Server CI / Lint (push) Failing after 2m42s
Server CI / Typecheck (push) Successful in 4m33s
syncModsFromPipeline resolved each mod's dependencies inline as it upserted
that mod, so a mod listed before its own dependency in catalog.json (e.g.
BroMaker, which appears before RocketLib) resolved against a Mod row that
didn't exist yet on a first-ever sync -- landing as "unresolved" until a
second sync run picked it up. Verified live: /community/mods/bromaker's
install plan showed both deps unresolved after the first sync, and
RocketLib correctly resolved (with its own transitive unresolved UMM dep)
after a second.

Split into two passes: upsert every Mod row first, then resolve
versions/dependencies against the now-complete set. Makes a single sync run
order-independent regardless of catalog.json's array order.
2026-08-03 01:31:35 -04:00
wdunn001
933ff37ccb community: M5 -- mods catalog and achievement definitions/display
Some checks failed
Server CI / Lint (push) Failing after 6m33s
Server CI / Typecheck (push) Successful in 7m42s
Adds the M5 slice of the community layer: Mod/ModVersion/ModDependency/
ModInstallation/ModInstalledFile/AchievementDefinition (migration
m5_mods_achievements). Gods/Users are referenced as plain soft columns
(gameId/uploaderId/identityId), not Prisma relations, following the
concurrent-edit-avoidance pattern devices.prisma established -- content.prisma
and user.prisma only grow a comment each.

Mods: syncs from script-library/scripts/archive-mod.py's _index/catalog.json
(read-only bind mount, homelab-compose/drop-stack/docker-compose.yml) into
Mod/ModVersion/ModDependency, resolves dependency refs into real Mod rows
where archived, and exposes a topologically-ordered install-plan resolver
with cycle/conflict/unresolved-dependency detection
(GET /mods/:id/versions/:v/plan). The install manifest
(ModInstallation/ModInstalledFile) records created/overwrote/skipped per
file so an uninstall plan is exact, computed server-side from what was
recorded, applied by the caller. Downloads stream directly off the mount
(GET /mods/:id/versions/:v/download) since cdn.quasarke.net has no
file_server wired to /mnt/local24/game-mods yet -- documented as a known gap
in server/internal/community/README.md, deliberately not fixed by editing
the live Caddyfile. A manual add form covers the DESIGN.md open-question #4
fallback (no upload write path exists).

Achievements: imports gbe_fork's archived generate_emu_config output
(/mnt/local24/game-tools/goldberg/schemas, also read-only mounted) via
goldbergAdapter.ts, copying icons into Drop's own object store
(objectHandler.createFromSource, same mechanism as Steam cover art) so
nothing is hotlinked. Display joins AchievementDefinition against M2's
already-landed AchievementUnlock by (steamAppId, apiName), exactly the join
key M2 designed it around -- no stub needed, M2's table was already in the
tree. Verified against real fixtures: appid 245170 (Skullgirls) importing
45/45 definitions with 90 icons, and 274190 (Broforce) importing 17/17.

Pages: /community/mods, /community/mods/:slug, /community/mods/new,
/community/achievements, /community/games/:gameId (achievement grid +
per-title mods, completion %). Small additive nav links on /community's
index rather than touching the actively-changing shared game-detail page.
2026-08-03 01:16:25 -04:00
wdunn001
c21e69aa5f community: M2 -- device enrollment, wrapper ingest, playtime, played-only listing
Some checks failed
Server CI / Lint (push) Failing after 3m38s
Server CI / Typecheck (push) Successful in 5m36s
Adds the M2 slice of the community layer: Device/DeviceToken/EnrollmentCode/
PlaySession/AchievementUnlock (migration m2_devices_playtime), the
device-token-authenticated ingest surface (POST /api/v1/ingest/enroll,
session-start, session-stop, achievements -- every success acks 2xx +
{"ok":true}, idempotent on sessionId), the /community/devices enrollment UI,
a PowerShell launch wrapper (homelab-compose/drop-stack/wrapper), and unified
playtime rails across Drop + RomM on the profile page.

Also implements the community titles listing change: default view is now
played-only (PlaySession + native Playtime, both platforms), sorted by most
recently played, with a friendly empty state and a "browse all" toggle back
to the full catalog. Adds a live-activity panel (now playing / recently
played) sourced from PlaySession, and a clearly-labeled server-status slot
for M4 to fill in.

PlaySession is the join table for M3/M4/M5: userId, deviceId, titlePlatform
('drop'|'romm'), gameId, communityTitleId, steamAppId, sessionId (unique),
startedAt, endedAt, durationSeconds, endReason, exitCode. No stale-session
sweeper in this pass -- "currently playing" is endedAt IS NULL, filtered to
sessions started within the last 12h for the live panel.
2026-08-03 01:08:39 -04:00
wdunn001
e20b029cb2 community: fix sync's P2029 param-limit crash, harden browse page
Some checks failed
Server CI / Lint (push) Failing after 5m34s
Server CI / Typecheck (push) Successful in 1h21m1s
runCommunityTitleSync's "hide titles that disappeared from RomM" step
built `externalId: { notIn: Array.from(seenExternalIds) }` -- on a
first sync essentially every title is "seen", so that list is ~34.5k
entries, each becoming a bound parameter and blowing Postgres's query
parameter limit (Prisma P2029: "query parameter limit... is exceeded").
Caught live: the first real "Sync now" click 500'd immediately.

Replaced with a timestamp comparison: capture syncStartedAt before any
row is touched, let every create/update set (or DB-default) lastSyncedAt
to now, then hide anything still older than syncStartedAt. Same "wasn't
touched by this run" semantics as the id-set check, O(1) parameters
regardless of catalog size.

Also hardens server/pages/community/index.vue's initial title fetch and
admin sync-status fetch with try/catch instead of an unguarded top-level
await -- a request that throws for any reason (this bug, a future one,
a transient adapter hiccup) should degrade to an empty/quiet state, not
take the whole page down via Nuxt's full-page error boundary. This is
exactly how a *different*, now-fixed bug (an arktype schema requiring
the `platform` query key to be present) surfaced: a plain 400 from the
API turned into a hard page crash purely because nothing caught it.
2026-08-02 14:13:32 -04:00
wdunn001
fc29101ba6 Dockerfile: cache torrential's cargo registry + target dir across builds
Any change anywhere in the repo invalidates torrential-build's
`COPY . .` (and therefore the cargo RUN layer after it) on every single
build, so without a persistent cache every build re-downloads and
recompiles every crate from zero regardless of whether torrential/
itself changed -- the ~50 minutes of crates.io traffic that motivated
the CARGO_NET_RETRY/CARGO_HTTP_LOW_SPEED_LIMIT hardening two commits
back gets paid again on every future commit to unrelated files (like
the previous commit's one-line server/ fix).

--mount=type=cache on the cargo registry and target/ dirs persists them
in the buildx cache store instead, independent of layer invalidation.
Cache mounts are NOT part of the final layer filesystem, so the built
binary is `cp`'d out to a stable path (/build/torrential-bin) before the
RUN ends; the run-system stage's COPY --from reads that stable path
instead of target/release/torrential directly.

sharing=locked because the capped builder can be asked to run more than
one image build at a time.
2026-08-02 13:24:12 -04:00
wdunn001
5efbd01880 community: fix titles.get.ts 400 on a bare /community browse request
Some checks failed
Server CI / Typecheck (push) Has been cancelled
Server CI / Lint (push) Has been cancelled
Arktype needs the KEY suffixed with "?" to make a query param
optional (same pattern already used correctly for `q: "string?"`,
which marks the VALUE nullable, not the key absent) -- marking only the
*value* type as "'drop' | 'romm' | undefined" still requires the
`platform` key to be present, just allowing it to hold undefined. Since
getQuery() simply omits absent query keys rather than setting them to
literal `undefined`, every browse request without an explicit platform
filter -- i.e. every normal page load of /community, both the initial
SSR request and the client-side fetch behind it -- failed arktype
validation with "platform must be 'drop', 'romm' or undefined (was
missing)" and 400'd. browseService.ts already treats platform as an
optional key (`platform?: "drop" | "romm"`) and handles it being absent
correctly (`wantsDrop = !platform || platform === "drop"`), so this was
purely a schema-declaration bug in the API route, not a design issue.

Caught by browser-driving drop.quasarke.net/community end-to-end (not
just curl -- a plain GET returns real content either way, so this
needed an actual page load to surface).
2026-08-02 13:22:48 -04:00
wdunn001
d46e4bf753 Dockerfile: harden torrential's cargo fetch against .88's bursty network
A capped from-source build on .88 got ~50 minutes into the torrential
Rust stage (all deps downloading fine) before one crates.io transfer
stalled below curl's default low-speed floor (10 bytes/s over 30s) long
enough to exhaust CARGO_NET_RETRY's default 3 attempts and hard-fail the
whole build (`failed to download from .../libc/0.2.178/download`, exit
101) -- losing all that download progress since cargo has no persistent
registry cache across a failed RUN layer.

Lower CARGO_HTTP_LOW_SPEED_LIMIT to 1 byte/s (a live-but-slow transfer on
a throttled host shouldn't read as dead), extend the low-speed timeout
and overall HTTP timeout, raise CARGO_NET_RETRY to 10, and disable HTTP/2
multiplexing (single-stream is more resilient to a lossy/bursty link than
multiplexed streams fighting over the same bandwidth).
2026-08-02 12:54:31 -04:00
wdunn001
874be15118 Dockerfile: add CARGO_JOBS + NODE_MAX_OLD_SPACE resource caps for capped builds
Third piece of the memory-pressure escape hatch (alongside SKIP_TYPECHECK):
--build-arg CARGO_JOBS=2 bounds cargo's parallel job count for the
torrential build, --build-arg NODE_MAX_OLD_SPACE=4096 bounds Node's
old-space heap for the postinstall/build step. Both default to unset
(no cap, existing behavior unchanged) -- only take effect when explicitly
passed, for building on a host with limited headroom under docker build
--cpus/--memory constraints.
2026-08-02 01:18:58 -04:00
wdunn001
cc5b8234e0 Dockerfile: add CARGO_JOBS + NODE_MAX_OLD_SPACE resource caps for capped builds
Third piece of the memory-pressure escape hatch (alongside SKIP_TYPECHECK):
--build-arg CARGO_JOBS=2 bounds cargo's parallel job count for the
torrential build, --build-arg NODE_MAX_OLD_SPACE=4096 bounds Node's
old-space heap for the postinstall/build step. Both default to unset
(no cap, existing behavior unchanged) -- only take effect when explicitly
passed, for building on a host with limited headroom under docker build
--cpus/--memory constraints.
2026-08-02 01:18:02 -04:00
wdunn001
0a41fd429e Dockerfile: add a memory-pressure escape hatch for the typecheck step
Some checks failed
Server CI / Typecheck (push) Failing after 3h14m3s
Server CI / Lint (push) Failing after 3h10m52s
Observed on .88 (2026-08-02): docker build got through deps install,
Prisma generate, and the client bundle fine, then got OOM-killed (exit 143)
during vue-tsc's full-project typecheck -- the host had ~600Mi free and a
fully-exhausted 2GB swap at the time (osrm-routed alone holds ~47GB RSS on
that shared box). This isn't a code problem; it's the single most
memory-hungry step of `nuxt build` landing on an already-saturated host.

--build-arg SKIP_TYPECHECK=true (env NUXT_BUILD_SKIP_TYPECHECK) skips it.
Off by default -- this is a resource-pressure escape hatch for a
contended host, not a reason to stop typechecking; re-run
`pnpm run typecheck` separately once there's headroom.
2026-08-02 01:03:09 -04:00
wdunn001
b43e9b2b60 Dockerfile: add a memory-pressure escape hatch for the typecheck step
Observed on .88 (2026-08-02): docker build got through deps install,
Prisma generate, and the client bundle fine, then got OOM-killed (exit 143)
during vue-tsc's full-project typecheck -- the host had ~600Mi free and a
fully-exhausted 2GB swap at the time (osrm-routed alone holds ~47GB RSS on
that shared box). This isn't a code problem; it's the single most
memory-hungry step of `nuxt build` landing on an already-saturated host.

--build-arg SKIP_TYPECHECK=true (env NUXT_BUILD_SKIP_TYPECHECK) skips it.
Off by default -- this is a resource-pressure escape hatch for a
contended host, not a reason to stop typechecking; re-run
`pnpm run typecheck` separately once there's headroom.
2026-08-02 01:02:40 -04:00
wdunn001
9e53e19480 docs(sites/docs): desktop client rebuild shipped for Windows -- update known-gaps
Cloud saves and the Community tab are no longer gaps; both work in the
Quasarke Edition build now served at cdn.quasarke.net/drop-client (upstream
kept at /upstream/). Documents honestly that Linux wasn't published this
pass -- the build host was too overloaded to finish it, not a source defect.
2026-08-02 01:01:01 -04:00
wdunn001
8a18e657f0 desktop: fix community telemetry to match the corrected architecture, fix a release-blocking borrow-checker error
Community was never a separate community.quasarke.net host -- it lives
inside the Drop server itself at {server}/community, same as the server-side
fold-in two commits up. The desktop client's own community.rs, models.rs,
and the two community.vue pages still assumed the old sidecar design
(Settings.communityUrl/communityEndpoint defaulting to
https://community.quasarke.net), so this corrects them to match:

- community.rs now posts to {server}/api/v1/ingest/{session-start,
  session-stop,achievements} (the fixed contract, matching the wrapper) via
  the same generate_url()/make_authenticated_post_json() helpers
  cloud_saves_commands.rs already uses, instead of an unauthenticated
  bespoke HTTP client pointed at a hardcoded second host. {server} is
  DB.base_url -- whatever Drop server the user actually signed into.
- database/src/models.rs: dropped the now-meaningless community_endpoint/
  community_url Settings fields; kept enable_community_telemetry as the
  only toggle.
- community.vue (the tab) and settings/community.vue: resolve the page URL
  via the existing gen_drop_url Tauri command (same one the admin-panel
  link already uses) instead of a stored/hardcoded URL.
- error.vue: the generic pre-auth error page can render before any server
  is configured, so it can't resolve a per-server community link the way
  community.vue does -- pointed its "Support" link at our docs instead of
  the retired community.quasarke.net.

Also fixes a real release-blocking bug in process_manager.rs found while
verifying this compiles: the drop://game-launch telemetry emit held
install_dir (a reference borrowed out of db_lock via game_status) live past
the db_lock.applications.transient_statuses.insert() mutable borrow a few
lines above it -- an E0502 that fails every build, release or debug.
Fixed by cloning install_dir to an owned String before that mutable borrow
point and using the clone in the emit instead.

Verified: cargo check -p drop-app passes; full `cargo tauri build` release
build succeeds and produces a working NSIS installer (7z archive-test:
"Everything is Ok").
2026-08-02 00:45:05 -04:00
wdunn001
f59fd03b47 community: fix source-build type errors (missing ACL descriptions, JSON cast)
Some checks are pending
Server CI / Typecheck (push) Waiting to run
Server CI / Lint (push) Waiting to run
Caught by an actual `docker build` of the fork from source on .88:
descriptions.ts requires an exhaustive description for every ACL name
(missed community:read/community:sync when adding them), and
titleSyncService's update() call needed the same Prisma.InputJsonValue
cast its createMany() sibling already had for the metadata column.
2026-08-02 00:34:51 -04:00
wdunn001
95f41c5c46 community: fix source-build type errors (missing ACL descriptions, JSON cast)
Caught by an actual `docker build` of the fork from source on .88:
descriptions.ts requires an exhaustive description for every ACL name
(missed community:read/community:sync when adding them), and
titleSyncService's update() call needed the same Prisma.InputJsonValue
cast its createMany() sibling already had for the metadata column.
2026-08-02 00:33:04 -04:00
wdunn001
4bec3b11ba docs(sites/docs): document that Community lives in Drop, not a sidecar
Some checks failed
Deploy website to GitHub Pages / build (push) Failing after 22s
Deploy website to GitHub Pages / deploy (push) Has been skipped
Community was originally documented as a planned separate service at
community.quasarke.net. It now ships as part of Drop itself at
drop.quasarke.net/community -- update the Quasarke Edition docs section
(overview, the community page's feature-status table and identity/chat
sections, the known-gaps note about the desktop client's stub tab, and the
Discord/community social link in astro.config.mjs) to match.
2026-08-02 00:17:33 -04:00
wdunn001
7da8ada4d6 docs(sites/docs): document that Community lives in Drop, not a sidecar
Community was originally documented as a planned separate service at
community.quasarke.net. It now ships as part of Drop itself at
drop.quasarke.net/community -- update the Quasarke Edition docs section
(overview, the community page's feature-status table and identity/chat
sections, the known-gaps note about the desktop client's stub tab, and the
Discord/community social link in astro.config.mjs) to match.
2026-08-02 00:16:28 -04:00
wdunn001
faa8b7d61c community: fold /community into the fork, source-build the three JS patches
Some checks are pending
Deploy website to GitHub Pages / deploy (push) Blocked by required conditions
Deploy website to GitHub Pages / build (push) Waiting to run
Server CI / Typecheck (push) Waiting to run
Server CI / Lint (push) Waiting to run
Replaces the server/pages/community.vue stub ({{ $t("todo") }}) with real
Nuxt pages: a unified Drop+RomM title browser, an admin catalog-sync panel,
and a profile page with playtime rails. Corrects the original architecture
mistake of building this as a separate service at community.quasarke.net --
now that we own the fork's source, community is ordinary Nuxt pages instead
of a sidecar with its own auth/DB/vhost.

Data model: one new table, CommunityTitle, for the RomM catalog (ported
from drop-community M1's Title spine). Drop's own Game/User/Playtime models
already ARE the title/identity/playtime spine for Drop content, so nothing
new was needed there -- profile playtime rails read real, already-existing
Playtime rows, no wrapper/ingest milestone required to populate them.
RommAdapter (server/server/internal/community/rommAdapter.ts) reads RomM's
MariaDB directly via the community_ro read-only role, same deviation and
same reasoning as M1's adapter (RomM's /api/roms needs a browser session,
no service-account key). TitleSyncService runs every 6h
(scheduledTasks.communityTitleSync) and via an admin "Sync now" button
(system:community:sync ACL) -- never deletes, hides instead.

Also folds the three post-build JS patches
(homelab-compose/drop-stack/patch-{launcher-button,telemetry-snippet,
footer-links}.js) into source now that the fork builds from source instead
of patching upstream's prebuilt image:
  - library/game/[id]/index.vue: the "Open in Launcher" button gets a real
    @click handler (drop://open with a timeout fallback to /download).
  - plugins/telemetry.client.ts: OpenPanel RUM injected via a Nuxt client
    plugin instead of appending to the compiled entry chunk.
  - UserFooter.vue + admin/library source-help links + error.vue + the
    Weblate link: repointed at our own infra. The Discord/social link
    used to point at community.quasarke.net; that service is retired, so
    it now points at /community directly.
2026-08-02 00:12:06 -04:00
wdunn001
92f053e51e community: fold /community into the fork, source-build the three JS patches
Replaces the server/pages/community.vue stub ({{ $t("todo") }}) with real
Nuxt pages: a unified Drop+RomM title browser, an admin catalog-sync panel,
and a profile page with playtime rails. Corrects the original architecture
mistake of building this as a separate service at community.quasarke.net --
now that we own the fork's source, community is ordinary Nuxt pages instead
of a sidecar with its own auth/DB/vhost.

Data model: one new table, CommunityTitle, for the RomM catalog (ported
from drop-community M1's Title spine). Drop's own Game/User/Playtime models
already ARE the title/identity/playtime spine for Drop content, so nothing
new was needed there -- profile playtime rails read real, already-existing
Playtime rows, no wrapper/ingest milestone required to populate them.
RommAdapter (server/server/internal/community/rommAdapter.ts) reads RomM's
MariaDB directly via the community_ro read-only role, same deviation and
same reasoning as M1's adapter (RomM's /api/roms needs a browser session,
no service-account key). TitleSyncService runs every 6h
(scheduledTasks.communityTitleSync) and via an admin "Sync now" button
(system:community:sync ACL) -- never deletes, hides instead.

Also folds the three post-build JS patches
(homelab-compose/drop-stack/patch-{launcher-button,telemetry-snippet,
footer-links}.js) into source now that the fork builds from source instead
of patching upstream's prebuilt image:
  - library/game/[id]/index.vue: the "Open in Launcher" button gets a real
    @click handler (drop://open with a timeout fallback to /download).
  - plugins/telemetry.client.ts: OpenPanel RUM injected via a Nuxt client
    plugin instead of appending to the compiled entry chunk.
  - UserFooter.vue + admin/library source-help links + error.vue + the
    Weblate link: repointed at our own infra. The Discord/social link
    used to point at community.quasarke.net; that service is retired, so
    it now points at /community directly.
2026-08-02 00:07:02 -04:00
wdunn001
c86b122101 desktop: wire the orphaned cloud_saves crate to the server saves API + a Saves tab
cloud_saves was a real, working crate (tar+zstd archive build/extract,
per-platform save-path translation) that was never reachable from the app:
a workspace member but not a drop-app dependency, no Tauri commands, no UI.
Adds six commands (cloud_saves_commands.rs) covering slot list/create/delete
against the server's existing /api/v1/client/saves/* routes (which the
client's handshake already declares the cloudSaves capability for -- see
remote/src/auth.rs) plus push/pull of an archive, and a Saves tab in the
game options modal to drive them.

One thing this does NOT do, because there is nothing to wire it to:
cloud_saves::resolver + BackupManager/CloudSaveMetadata/GameFile expect an
auto-detected, Ludusavi-style per-game save-file manifest
(CloudSaveMetadata.files), but nothing in Drop produces that manifest --
not the GameVersion Prisma model, not any admin UI, not any API response.
So push/pull here archive a directory the user picks explicitly rather than
auto-locating save files. The archive format (tar under a "data/" prefix,
zstd-compressed) stays compatible with what cloud_saves::resolver writes,
so a future manifest-driven auto-detect pass can still read what gets
pushed today.

Also: Drop's saves API only exposes a push (upload) route server-side, no
saves-specific download -- pull reuses the same authenticated
/api/v1/client/object/{id} route the client already uses for depot/image
assets, fetching the most recent entry in a slot's historyObjectIds.
2026-08-01 23:33:43 -04:00
wdunn001
4dda2f50af desktop: rebrand fork links, add community tab + native session/achievement telemetry
Retarget every upstream Drop-OSS/discord/droposs.org reference to our own
infra (github.com/Drop-OSS -> devops.quasarke.net/drop-oss, Discord ->
community.quasarke.net), rename the product to "Drop Desktop Client
(Quasarke Edition)" with a distinct app identifier so it can't collide with
a genuine upstream install, and note in updates.rs that there was never a
self-updater to repoint (no tauri-plugin-updater dependency exists anywhere
in this client -- confirmed via Cargo.toml/tauri.conf.json).

Community tab (desktop/main/pages/community.vue) replaces the upstream
"Under construction" stub: opens community.quasarke.net in the system
browser via tauri-plugin-shell (already covered by the existing
shell:allow-open capability, no CSP/capability changes needed).

Native play-session + achievements telemetry (desktop/src-tauri/src/community.rs):
process_manager now emits drop://game-launch / drop://game-exit tauri events
around every game launch, picked up by listeners in src/lib.rs that POST
session-start / session-stop / achievements to the community service,
matching homelab-compose's drop-launch-wrapper.ps1 contract exactly
(same endpoint paths, same {"ok":true} ack requirement, same
%LOCALAPPDATA%\drop-community\queue offline-queue format/location so a
machine running both the client and the per-game wrapper shares one
flush point). This gives every game launched through the client the same
tracking the wrapper gives individually-wrapped games, without requiring
the wrapper to be installed.

The community endpoint is a single configurable Settings field
(communityEndpoint/communityUrl/enableCommunityTelemetry in
database/src/models.rs, exposed via a new Settings > Community tab), not
hardcoded at each call site.
2026-08-01 23:32:38 -04:00