Skip to content

Commit db9d07b

Browse files
committed
update icon api. Add icons to tournaments, leaderboards, etc.
1 parent c636568 commit db9d07b

327 files changed

Lines changed: 17450 additions & 11637 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@
8181
# Directory containing OTP hook plugins.
8282
# GAMEND_CONTENT_PLUGINS_DIR=modules/plugins
8383

84-
# Base path of the theme JSON. Only locale-suffixed files are loaded
85-
# (.en.json, ...).
84+
# Path to the theme JSON. A single file serves every locale; its text is
85+
# translated via the gettext `theme` domain.
8686
# GAMEND_CONTENT_THEME_CONFIG=
8787

8888
# ──────────────────────────────────────────────────────────────────────────
@@ -314,7 +314,7 @@
314314
# ──────────────────────────────────────────────────────────────────────────
315315

316316
# Record a durable per-run snapshot of lobby state, browsable at
317-
# /admin/lobby-snapshots.
317+
# /admin/lobby_snapshots.
318318
# GAMEND_LOBBY_SNAPSHOTS_ENABLED=false
319319

320320
# Cap on KV entries captured per snapshot.

.github/copilot-instructions.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ LiveViews that require login should **always be placed inside the __existing__ `
103103
on_mount: [{GameServerWeb.UserAuth, :require_authenticated}] do
104104
# phx.gen.auth generated routes
105105
live "/users/settings", UserLive.Settings, :edit
106-
live "/users/settings/confirm-email/:token", UserLive.Settings, :confirm_email
106+
live "/users/settings/confirm_email/:token", UserLive.Settings, :confirm_email
107107
# our own routes that require logged in user
108108
live "/", MyLiveThatRequiresAuth, :index
109109
end
@@ -228,7 +228,7 @@ API routes use JWT tokens via Guardian for stateless authentication:
228228
- Push context: `GameServer.Push` — token registry (`register_token/2` upsert, `list_tokens/2`, `delete_token/2`, soft `disable_token/1`) + server-authoritative delivery (`send_to_user/3`, `send_to_users/3`; no public send endpoint).
229229
- Delivery routes **per token** off `push_tokens.provider` ("fcm" | "apns") through Pigeon dispatchers; unconfigured providers fall back to the zero-config `Log` provider. One Oban job per token on the `push` queue (`GAMEND_PUSH_QUEUE_CONCURRENCY`); large fan-outs expand via `FanoutWorker`.
230230
- Hooks: `before_push_send/2` (per-recipient veto/rewrite pipeline), `after_push_sent/3` (per-token outcome). `Notifications` bridges committed notifications to push for offline users via the cached `user_has_live_tokens?/1`.
231-
- API: `POST/GET /api/v1/me/push-tokens`, `DELETE /api/v1/me/push-tokens/:id`; admin under `/api/v1/admin/push/*`; admin page at `/admin/push`. Stale tokens prune via `GAMEND_RETENTION_PUSH_TOKENS_DAYS` (default 270).
231+
- API: `POST/GET /api/v1/me/push_tokens`, `DELETE /api/v1/me/push_tokens/:id`; admin under `/api/v1/admin/push/*`; admin page at `/admin/push`. Stale tokens prune via `GAMEND_RETENTION_PUSH_TOKENS_DAYS` (default 270).
232232

233233
### Notifications
234234

.github/workflows/build-and-check.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ jobs:
238238
run: |
239239
MIX_ENV=dev mix gamend.settings.env_example --check
240240
MIX_ENV=dev mix gamend.settings.guide --check
241+
MIX_ENV=dev mix gamend.theme.extract --check
241242
242243
- name: Audit dependencies for known vulnerabilities
243244
run: MIX_ENV=dev mix deps.audit

CHANGELOG.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,72 @@
11
# July 2026
22

3+
- [breaking] **The theme is one file, translated through gettext.**
4+
`theme/config.json` replaces the 30 per-locale copies, and its text is
5+
translated via the `theme` gettext domain. Two thirds of each copy was
6+
structure rather than text, and they drifted — a `theme_color` added to
7+
English never reached the other 29. Configuration (colours, hrefs, icons,
8+
layout) is now structurally untranslatable. Extract with
9+
`mix gamend.theme.extract`; migrate existing per-locale files with
10+
`mix gamend.theme.migrate_locales`. `modules/example_config*.json` is gone —
11+
nothing loaded it.
12+
- [breaking] **Quest and leaderboard translations are no longer stored in the
13+
database.** The per-locale `metadata["titles"][locale]` /
14+
`["descriptions"][locale]` copies, their admin editor and
15+
`Quest.localized_title/2` (plus the `Leaderboard` equivalents) are removed —
16+
the same duplicate-store problem the theme had. A row now holds only its
17+
source text and gettext translates it at render.
18+
`mix gamend.content.migrate_metadata` lifts existing per-locale metadata into
19+
`content.po` (`--dry-run` to preview, `--prune` to clear the dead keys).
20+
- [added] **Quest, leaderboard and tournament text is translatable.**
21+
`mix gamend.content.extract` reads the titles and descriptions from the
22+
database into a `content` domain, so plugin-defined *and* admin-created rows
23+
are covered; `GameServerWeb.ContentText` translates them per viewer. Admin
24+
pages deliberately keep showing the stored source string.
25+
- [added] **All 30 locales are fully translated** (352 strings each, up from
26+
266) — machine-translated through the CSV round-trip and awaiting native
27+
review.
28+
- [changed] `mix gettext.export_csv` / `import_csv` cover both gettext trees
29+
and every domain; the `_config` pseudo-domain and `--config` flag are gone,
30+
since theme text is now a normal PO domain.
31+
- [added] **Quest chains are browsable** — clicking a chained quest shows every
32+
tier, including locked ones ahead.
33+
- [breaking] **A chain lists as one quest.** `list_user_quests` (and
34+
`GET /quests/me`) return a single entry per chain — the earliest tier not yet
35+
claimed; once all are claimed, the final tier. Previously a finished tier and
36+
its unlocked successor listed side by side.
37+
- [breaking] **String fields in API responses are never `null`** — an unset
38+
value serializes as `""` (Godot clients choke on `null` where a string is
39+
expected). Applies to REST, realtime payloads and struct-encoded responses
40+
alike: `icon_url`, `description`, `recur`, `prerequisite_quest_key`,
41+
notification `sender_id`, and friends. Datetimes/numbers keep `null` where
42+
absence is semantic.
43+
- [added] `icon_url` on **notifications, tournaments, groups and leaderboards**
44+
(quests already had it). Group admins upload via `POST /groups/:id/icon/upload_url` +
45+
`POST /groups/:id/icon`; tournaments and leaderboards take a URL in admin.
46+
Entities without an icon fall back to one shared default per type, from the
47+
typed icon set in `GameServerWeb.Icons`.
48+
- [breaking] **API route paths use underscores throughout.** The API mixed the
49+
two styles (`/me/push-tokens` next to `/groups/:id/join_requests`); every
50+
hyphenated path is renamed, with no redirects. Notably `/users/log-in`,
51+
`/users/log-out`, `/users/update-password`,
52+
`/users/settings/confirm-email/:token`, `/me/push-tokens`,
53+
`/me/friend-requests`, `/me/avatar/upload-url`, `/data-deletion`,
54+
`/tournaments/:id/my-match`, `/economy/grant-item`, `/economy/consume-item`
55+
and the `/admin/rate-limiting` and `/admin/lobby-snapshots` pages. **Update
56+
any external registration of `/data-deletion`** (Meta/Facebook app settings),
57+
and note that confirmation links already emailed under the old path will 404.
58+
- [added] **Two-step icon upload for tournaments, leaderboards and quests**
59+
(admins), matching the flow groups and avatars already had:
60+
`POST .../icon/upload_url` returns a presigned ticket, the client PUTs the
61+
image straight to storage, `POST .../icon` confirms the key. The mechanism
62+
now lives in one place, `GameServerWeb.Uploads`, which also confines a
63+
client-supplied key to its own entity's prefix.
64+
- [fixed] Admin `GET`/`PATCH /api/v1/admin/tournaments` documented `icon_url`
65+
in its OpenAPI schema but never returned it.
66+
- [fixed] **Uploaded icons are cached like avatars.** `icons/` now carries the
67+
same immutable one-year `Cache-Control` as `avatars/` — every icon key is
68+
content-unique (`Storage.build_key/3`), so revalidating one on each page load
69+
was pure waste.
370
- [added] **Settings**: one declared config surface.
471
- [changed] **Guides are markdown files.** `/docs/setup`
572
- [changed] **Times are shown in the reader's timezone.**

Dockerfile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,16 @@ COPY apps/game_server_core/mix.exs apps/game_server_core/mix.exs
3939
# Install dependencies
4040
RUN mix deps.get
4141

42+
# Compile-time config must be present before deps compile (mdex reads its
43+
# syntax highlighter from config). Copied on its own so the layer only busts
44+
# when config changes, not on every source commit.
45+
COPY config config
46+
47+
# Compile external deps (hex + git, including the Rust NIFs) in a cached
48+
# layer. Without this, `COPY . .` below invalidated `mix compile` on every
49+
# commit and all ~140 deps rebuilt from scratch each CI run. Local path deps
50+
# (apps/*) are skipped: their sources arrive with COPY and compile next.
51+
RUN mix deps.compile --skip-local-deps
4252

4353
COPY . .
4454

ROADMAP.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ STORAGE_MAX_UPLOAD_BYTES=5242880
235235
```
236236

237237
**Upload flow (client-agnostic, same for Godot/JS):**
238-
1. Client asks `POST /me/avatar/upload-url` → server validates size/type,
238+
1. Client asks `POST /me/avatar/upload_url` → server validates size/type,
239239
returns `{url, method, headers, key}` from `presigned_upload/2`.
240240
2. Client uploads bytes directly to `url` (S3/R2 or the local endpoint).
241241
3. Client `POST /me/avatar {key}` → server verifies the object exists and
@@ -311,7 +311,7 @@ current codebase, and the CONTRIBUTING checklist.
311311
`GameServer.Push` fan-out on the Oban `push` queue; FCM + APNs-direct behind
312312
one behaviour, routed per token, delivered via **Pigeon 2**.
313313
- [Push — Godot client](docs/specs/push-godot-client.md) — Android (FCM) then iOS
314-
(native APNs) behind one `GamendPush.gd`, registering to `/me/push-tokens`.
314+
(native APNs) behind one `GamendPush.gd`, registering to `/me/push_tokens`.
315315
- [Chat moderation](docs/specs/chat-moderation.md) — word filter + report queue +
316316
mute, enforced in the existing `before_chat_message` pipeline.
317317

apps/game_server_core/config/config.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import Config
33
config :game_server_core, ecto_repos: [GameServer.Repo]
44

55
default_adapter =
6-
if System.get_env("DATABASE_ADAPTER") == "postgres",
6+
if System.get_env("GAMEND_DB_ADAPTER") == "postgres",
77
do: Ecto.Adapters.Postgres,
88
else: Ecto.Adapters.SQLite3
99

apps/game_server_core/lib/game_server/accounts.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1830,7 +1830,7 @@ defmodule GameServer.Accounts do
18301830
18311831
## Examples
18321832
1833-
iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm-email/#{&1}"))
1833+
iex> deliver_user_update_email_instructions(user, current_email, &url(~p"/users/settings/confirm_email/#{&1}"))
18341834
{:ok, %{to: ..., body: ...}}
18351835
18361836
"""

apps/game_server_core/lib/game_server/content_settings.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ defmodule GameServer.ContentSettings do
1010
label: "Content & plugins"
1111

1212
setting(:theme_config, :string,
13-
doc: "Base path of the theme JSON. Only locale-suffixed files are loaded (.en.json, ...)."
13+
doc:
14+
"Path to the theme JSON. A single file serves every locale; its text is translated via the gettext `theme` domain."
1415
)
1516

1617
setting(:plugins_dir, :string,

apps/game_server_core/lib/game_server/economy/ledger_entry.ex

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,6 @@ defmodule GameServer.Economy.LedgerEntry do
99

1010
@type t :: %__MODULE__{}
1111

12-
@derive {Jason.Encoder,
13-
only: [
14-
:id,
15-
:user_id,
16-
:currency,
17-
:delta,
18-
:balance_after,
19-
:reason,
20-
:metadata,
21-
:inserted_at
22-
]}
23-
2412
schema "ledger_entries" do
2513
belongs_to :user, GameServer.Accounts.User
2614
field :currency, :string
@@ -53,3 +41,24 @@ defmodule GameServer.Economy.LedgerEntry do
5341
|> unique_constraint(:idempotency_key, name: :ledger_entries_idempotency_key_index)
5442
end
5543
end
44+
45+
# Hand-written rather than @derive so nil strings encode as "" (see
46+
# GameServer.SchemaJSON — game clients choke on null).
47+
defimpl Jason.Encoder, for: GameServer.Economy.LedgerEntry do
48+
def encode(entry, opts) do
49+
GameServer.SchemaJSON.encode(
50+
entry,
51+
[
52+
:id,
53+
:user_id,
54+
:currency,
55+
:delta,
56+
:balance_after,
57+
:reason,
58+
:metadata,
59+
:inserted_at
60+
],
61+
opts
62+
)
63+
end
64+
end

0 commit comments

Comments
 (0)