diff --git a/docs/MOBILE_API.md b/docs/MOBILE_API.md
index 2abcdaca3..7472c3fe5 100644
--- a/docs/MOBILE_API.md
+++ b/docs/MOBILE_API.md
@@ -1,6 +1,6 @@
# EmuReady Public Integration API (mobile-compatible tRPC)
-*Auto-generated on: 2026-06-15T12:16:27.168Z*
+*Auto-generated on: 2026-07-06T20:07:17.160Z*
## Summary
- **Total Endpoints**: 113
@@ -12,7 +12,7 @@
`/api/mobile/trpc`
## Authentication
-Protected endpoints require Bearer token authentication using Clerk JWT.
+Protected endpoints require Bearer token authentication using Clerk JWT. Public integration requests can also include an issued API key in `x-api-key`.
## Interactive Documentation
- **Swagger UI**: [/docs/api/swagger](https://emuready.com/docs/api/swagger)
@@ -33,7 +33,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 2. **getDeviceCompatibility**
- **Method**: GET
- **Path**: `/catalog.getDeviceCompatibility`
-- **Description**: Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 10 minutes to reduce server load.
+- **Description**: Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 15 minutes to reduce server load.
- **Tags**: catalog
@@ -208,7 +208,7 @@ Protected endpoints require Bearer token authentication using Clerk JWT.
#### 27. **batchBySteamAppIds**
- **Method**: GET
- **Path**: `/games.batchBySteamAppIds`
-- **Description**: Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 5 minutes to optimize repeated queries
+- **Description**: Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 15 minutes to optimize repeated queries
- **Tags**: games
diff --git a/next.config.ts b/next.config.ts
index affdc29e9..2d2dbc6d3 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -280,24 +280,6 @@ const nextConfig: NextConfig = {
source: '/favicon/:path*',
headers: [{ key: 'Cache-Control', value: 'public, max-age=86400, must-revalidate' }],
},
- {
- source: '/api/mobile/:path*',
- headers: [
- { key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' },
- { key: 'Access-Control-Allow-Origin', value: '*' },
- { key: 'Access-Control-Allow-Methods', value: 'GET, POST, PUT, DELETE, OPTIONS' },
- {
- key: 'Access-Control-Allow-Headers',
- value: 'Content-Type, Authorization, x-trpc-source',
- },
- { key: 'Access-Control-Expose-Headers', value: 'x-trpc-source' },
- ],
- },
- // tRPC endpoints are dynamic; prevent intermediary/proxy caching
- {
- source: '/api/trpc/:path*',
- headers: [{ key: 'Cache-Control', value: 'no-store, no-cache, must-revalidate' }],
- },
{
source: '/(.*)',
headers: [
diff --git a/public/api-docs/mobile-openapi.json b/public/api-docs/mobile-openapi.json
index f7f24bab1..1109ec888 100644
--- a/public/api-docs/mobile-openapi.json
+++ b/public/api-docs/mobile-openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.0.0",
"info": {
"title": "EmuReady Public Integration API (mobile-compatible tRPC)",
- "description": "\n# EmuReady Public Integration tRPC API\n\nAPI documentation for the mobile-compatible public integration surface built with tRPC.\n\n## tRPC HTTP Method Conventions\n\nNOTE: the protected routes require authentication via Clerk JWT token in the Authorization header. This isn't implemented yet.\n\ntRPC uses HTTP method semantics with fetchRequestHandler:\n- **Queries** use **GET** requests with input as query parameter\n- **Mutations** use **POST** requests with input in request body\n\n### Schema References:\n\nAll input schemas are defined in the **components/schemas** section. When you see a parameter referencing a schema (e.g., GetEmulatorsSchema), check the schemas section for the complete structure with field types, validations, and defaults.\n\n### Usage Examples:\n\n```bash\n# Query: Get games with search and limit (GET with SuperJSON wrapped input)\n# Schema: See components/schemas/GetGamesSchema\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/games.getGames?input=%7B%22json%22%3A%7B%22search%22%3A%22mario%22%2C%22limit%22%3A5%7D%7D\" \\\n -H \"Content-Type: application/json\"\n\n# Query: Get popular games (GET, no input required)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/games.getPopularGames\" \\\n -H \"Content-Type: application/json\"\n\n# Query: Get listings with filters (GET with SuperJSON wrapped input)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/listings.getListings?input=%7B%22json%22%3A%7B%22page%22%3A1%2C%22limit%22%3A10%2C%22search%22%3A%22zelda%22%7D%7D\" \\\n -H \"Content-Type: application/json\"\n\n# Mutation: Create listing (POST with request body)\ncurl -X POST \"https://www.emuready.com/api/mobile/trpc/listings.createListing\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer YOUR_JWT_TOKEN\" \\\n -d '{\"gameId\":\"uuid\",\"deviceId\":\"uuid\",\"emulatorId\":\"uuid\",\"performanceId\":\"uuid\"}'\n\n# Protected query with authentication (GET with SuperJSON wrapped input and auth header)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/listings.getUserListings?input=%7B%22json%22%3A%7B%22userId%22%3A%22uuid%22%7D%7D\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n### Important Notes:\n\n**For Queries (GET requests):**\n✅ Use GET method\n✅ Send input wrapped in SuperJSON format: `{\"json\":{\"field\":\"value\"}}`\n✅ URL-encode the entire JSON string\n✅ Many endpoints have defaults and don't require input\n✅ Input parameter format: `?input={\"json\":{\"field\":\"value\"}}` (URL-encoded)\n\n**For Mutations (POST requests):**\n✅ Use POST method\n✅ Send input as JSON in request body\n✅ Set Content-Type: application/json\n\n### Response Format:\nAll responses are wrapped in a tRPC result object:\n```json\n{\n \"result\": {\n \"data\": /* response data */\n }\n}\n```\n\n### Error Response Format:\n```json\n{\n \"error\": {\n \"json\": {\n \"message\": \"Error message\",\n \"code\": -32600,\n \"data\": {\n \"code\": \"BAD_REQUEST\",\n \"httpStatus\": 400,\n \"path\": \"games.getGames\"\n }\n }\n }\n}\n```\n\nThis API provides endpoints for:\n- Game emulation listings management\n- User authentication and profiles \n- Device and hardware information\n- Emulator data and compatibility\n- Community features (comments, votes)\n ",
+ "description": "\n# EmuReady Public Integration tRPC API\n\nAPI documentation for the mobile-compatible public integration surface built with tRPC.\n\n## tRPC HTTP Method Conventions\n\nProtected routes require authentication via Clerk JWT token in the Authorization header. Public integration requests can also include an issued API key in `x-api-key` for attribution and quota tracking. Invalid `Authorization: ApiKey` credentials are rejected.\n\ntRPC uses HTTP method semantics with fetchRequestHandler:\n- **Queries** use **GET** requests with input as query parameter\n- **Mutations** use **POST** requests with input in request body\n\n### Schema References:\n\nAll input schemas are defined in the **components/schemas** section. When you see a parameter referencing a schema (e.g., GetEmulatorsSchema), check the schemas section for the complete structure with field types, validations, and defaults.\n\n### Usage Examples:\n\n```bash\n# Query: Get games with search and limit (GET with SuperJSON wrapped input)\n# Schema: See components/schemas/GetGamesSchema\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/games.get?input=%7B%22json%22%3A%7B%22search%22%3A%22mario%22%2C%22limit%22%3A5%7D%7D\" \\\n -H \"Content-Type: application/json\"\n\n# Query: Get popular games (GET, no input required)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/games.getPopularGames\" \\\n -H \"Content-Type: application/json\"\n\n# Query: Get listings with filters (GET with SuperJSON wrapped input)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/listings.get?input=%7B%22json%22%3A%7B%22page%22%3A1%2C%22limit%22%3A10%2C%22search%22%3A%22zelda%22%7D%7D\" \\\n -H \"Content-Type: application/json\"\n\n# Mutation: Create listing (POST with request body)\ncurl -X POST \"https://www.emuready.com/api/mobile/trpc/listings.createListing\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer YOUR_JWT_TOKEN\" \\\n -d '{\"gameId\":\"uuid\",\"deviceId\":\"uuid\",\"emulatorId\":\"uuid\",\"performanceId\":\"uuid\"}'\n\n# Protected query with authentication (GET with SuperJSON wrapped input and auth header)\ncurl -X GET \"https://www.emuready.com/api/mobile/trpc/listings.getUserListings?input=%7B%22json%22%3A%7B%22userId%22%3A%22uuid%22%7D%7D\" \\\n -H \"Content-Type: application/json\" \\\n -H \"Authorization: Bearer YOUR_JWT_TOKEN\"\n```\n\n### Important Notes:\n\n**For Queries (GET requests):**\n✅ Use GET method\n✅ Send input wrapped in SuperJSON format: `{\"json\":{\"field\":\"value\"}}`\n✅ URL-encode the entire JSON string\n✅ Many endpoints have defaults and don't require input\n✅ Input parameter format: `?input={\"json\":{\"field\":\"value\"}}` (URL-encoded)\n\n**For Mutations (POST requests):**\n✅ Use POST method\n✅ Send input as JSON in request body\n✅ Set Content-Type: application/json\n\n### Response Format:\nAll responses are wrapped in a tRPC result object:\n```json\n{\n \"result\": {\n \"data\": /* response data */\n }\n}\n```\n\n### Error Response Format:\n```json\n{\n \"error\": {\n \"json\": {\n \"message\": \"Error message\",\n \"code\": -32600,\n \"data\": {\n \"code\": \"BAD_REQUEST\",\n \"httpStatus\": 400,\n \"path\": \"games.get\"\n }\n }\n }\n}\n```\n\nThis API provides endpoints for:\n- Game emulation listings management\n- User authentication and profiles \n- Device and hardware information\n- Emulator data and compatibility\n- Community features (comments, votes)\n ",
"version": "1.0.0",
"contact": {
"name": "EmuReady API Support",
@@ -65,7 +65,7 @@
},
"path": {
"type": "string",
- "description": "tRPC procedure path (e.g., \"games.getGames\")"
+ "description": "tRPC procedure path (e.g., \"games.get\")"
},
"zodError": {
"type": "object",
@@ -163,10 +163,12 @@
},
"deviceModelName": {
"type": "string",
+ "maxLength": 120,
"description": "Device model name (e.g., \"Pocket 5\")"
},
"deviceBrandName": {
"type": "string",
+ "maxLength": 80,
"description": "Device brand name (e.g., \"Retroid\")"
},
"systemIds": {
@@ -175,6 +177,7 @@
"type": "string",
"format": "uuid"
},
+ "maxItems": 100,
"description": "Filter results to specific system IDs"
},
"includeEmulatorBreakdown": {
@@ -185,6 +188,7 @@
"minListingCount": {
"type": "number",
"minimum": 0,
+ "maximum": 100,
"default": 1,
"description": "Minimum number of listings required to include a system"
}
@@ -451,6 +455,7 @@
"properties": {
"search": {
"type": "string",
+ "maxLength": 100,
"description": "Search devices by name"
},
"brandId": {
@@ -479,7 +484,8 @@
"format": "uuid"
},
"search": {
- "type": "string"
+ "type": "string",
+ "maxLength": 100
},
"limit": {
"type": "number",
@@ -512,7 +518,8 @@
"type": "object",
"properties": {
"search": {
- "type": "string"
+ "type": "string",
+ "maxLength": 100
},
"systemId": {
"type": "string",
@@ -546,7 +553,8 @@
"properties": {
"query": {
"type": "string",
- "minLength": 1
+ "minLength": 1,
+ "maxLength": 100
}
},
"required": [
@@ -572,7 +580,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
},
"maxResults": {
"type": "number",
@@ -591,7 +600,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
}
},
"required": [
@@ -616,7 +626,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
},
"maxResults": {
"type": "number",
@@ -635,7 +646,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
}
},
"required": [
@@ -660,7 +672,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
},
"maxResults": {
"type": "number",
@@ -679,7 +692,8 @@
"properties": {
"gameName": {
"type": "string",
- "minLength": 2
+ "minLength": 2,
+ "maxLength": 120
}
},
"required": [
@@ -720,6 +734,7 @@
},
"emulatorName": {
"type": "string",
+ "maxLength": 100,
"description": "Filter listings by emulator name"
},
"maxListingsPerGame": {
@@ -745,12 +760,661 @@
],
"additionalProperties": false
},
+ "BatchBySteamAppIdsResponseSchema": {
+ "type": "object",
+ "properties": {
+ "success": {
+ "type": "boolean",
+ "const": true
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "steamAppId": {
+ "type": "string"
+ },
+ "game": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "normalizedTitle": {
+ "type": "string",
+ "nullable": true
+ },
+ "systemId": {
+ "type": "string"
+ },
+ "imageUrl": {
+ "type": "string",
+ "nullable": true
+ },
+ "boxartUrl": {
+ "type": "string",
+ "nullable": true
+ },
+ "bannerUrl": {
+ "type": "string",
+ "nullable": true
+ },
+ "tgdbGameId": {
+ "type": "number",
+ "nullable": true
+ },
+ "metadata": {},
+ "isErotic": {
+ "type": "boolean"
+ },
+ "ageRating": {
+ "type": "string",
+ "nullable": true
+ },
+ "status": {
+ "type": "string"
+ },
+ "createdAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "system": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "key": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "key"
+ ],
+ "additionalProperties": true
+ },
+ "_count": {
+ "type": "object",
+ "properties": {
+ "listings": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "listings"
+ ],
+ "additionalProperties": true
+ },
+ "listings": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "notes": {
+ "type": "string",
+ "nullable": true
+ },
+ "upvoteCount": {
+ "type": "number"
+ },
+ "downvoteCount": {
+ "type": "number"
+ },
+ "voteCount": {
+ "type": "number"
+ },
+ "successRate": {
+ "type": "number",
+ "nullable": true
+ },
+ "deviceId": {
+ "type": "string"
+ },
+ "gameId": {
+ "type": "string"
+ },
+ "emulatorId": {
+ "type": "string"
+ },
+ "performanceId": {
+ "type": "number"
+ },
+ "device": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "modelName": {
+ "type": "string"
+ },
+ "soc": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "manufacturer": {
+ "type": "string",
+ "nullable": true
+ },
+ "architecture": {
+ "type": "string",
+ "nullable": true
+ },
+ "processNode": {
+ "type": "string",
+ "nullable": true
+ },
+ "cpuCores": {
+ "type": "number",
+ "nullable": true
+ },
+ "gpuModel": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "manufacturer",
+ "architecture",
+ "processNode",
+ "cpuCores",
+ "gpuModel"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "modelName",
+ "soc"
+ ],
+ "additionalProperties": true
+ },
+ "emulator": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "logo": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "logo"
+ ],
+ "additionalProperties": true
+ },
+ "performance": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "number"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "label",
+ "rank",
+ "description"
+ ],
+ "additionalProperties": true
+ },
+ "customFieldValues": {
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "listingId": {
+ "type": "string"
+ },
+ "customFieldDefinitionId": {
+ "type": "string"
+ },
+ "value": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "array",
+ "items": {}
+ },
+ {
+ "type": "object",
+ "additionalProperties": {}
+ }
+ ]
+ }
+ },
+ {
+ "type": "object",
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "null"
+ },
+ {
+ "type": "array",
+ "items": {}
+ },
+ {
+ "type": "object",
+ "additionalProperties": {}
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "customFieldDefinition": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string"
+ },
+ "label": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "id",
+ "type",
+ "label",
+ "name"
+ ],
+ "additionalProperties": true
+ }
+ },
+ "required": [
+ "id",
+ "listingId",
+ "customFieldDefinitionId",
+ "value",
+ "customFieldDefinition"
+ ],
+ "additionalProperties": true
+ }
+ }
+ },
+ "required": [
+ "id",
+ "notes",
+ "upvoteCount",
+ "downvoteCount",
+ "voteCount",
+ "successRate",
+ "deviceId",
+ "gameId",
+ "emulatorId",
+ "performanceId",
+ "device",
+ "emulator",
+ "performance",
+ "customFieldValues"
+ ],
+ "additionalProperties": true
+ }
+ }
+ },
+ "required": [
+ "id",
+ "title",
+ "systemId",
+ "imageUrl",
+ "boxartUrl",
+ "bannerUrl",
+ "tgdbGameId",
+ "isErotic",
+ "status",
+ "createdAt",
+ "system",
+ "_count",
+ "listings"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "matchStrategy": {
+ "type": "string",
+ "enum": [
+ "metadata",
+ "exact",
+ "normalized",
+ "not_found"
+ ]
+ }
+ },
+ "required": [
+ "steamAppId",
+ "game",
+ "matchStrategy"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "object",
+ "properties": {
+ "game_id": {
+ "type": "string",
+ "nullable": true
+ },
+ "steam_app_id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string",
+ "nullable": true
+ },
+ "performance": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "number"
+ },
+ "label": {
+ "type": "string"
+ },
+ "rank": {
+ "type": "number"
+ },
+ "description": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "label",
+ "rank",
+ "description"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "emulator": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "logo": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "logo"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "device": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "modelName": {
+ "type": "string"
+ },
+ "soc": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "manufacturer": {
+ "type": "string",
+ "nullable": true
+ },
+ "architecture": {
+ "type": "string",
+ "nullable": true
+ },
+ "processNode": {
+ "type": "string",
+ "nullable": true
+ },
+ "cpuCores": {
+ "type": "number",
+ "nullable": true
+ },
+ "gpuModel": {
+ "type": "string",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "name",
+ "manufacturer",
+ "architecture",
+ "processNode",
+ "cpuCores",
+ "gpuModel"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "id",
+ "modelName",
+ "soc"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ },
+ "listing": {
+ "anyOf": [
+ {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "string",
+ "nullable": true
+ },
+ "notes": {
+ "type": "string",
+ "nullable": true
+ },
+ "upvoteCount": {
+ "type": "number"
+ },
+ "downvoteCount": {
+ "type": "number"
+ },
+ "voteCount": {
+ "type": "number"
+ },
+ "successRate": {
+ "type": "number",
+ "nullable": true
+ }
+ },
+ "required": [
+ "id",
+ "notes",
+ "upvoteCount",
+ "downvoteCount",
+ "voteCount",
+ "successRate"
+ ],
+ "additionalProperties": true
+ },
+ {
+ "type": "null"
+ }
+ ]
+ }
+ },
+ "required": [
+ "game_id",
+ "steam_app_id",
+ "title",
+ "performance",
+ "emulator",
+ "device",
+ "listing"
+ ],
+ "additionalProperties": true
+ }
+ ]
+ }
+ },
+ "totalRequested": {
+ "type": "number"
+ },
+ "totalFound": {
+ "type": "number"
+ },
+ "totalNotFound": {
+ "type": "number"
+ }
+ },
+ "required": [
+ "success",
+ "results",
+ "totalRequested",
+ "totalFound",
+ "totalNotFound"
+ ],
+ "additionalProperties": true
+ },
"SearchSuggestionsSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
- "minLength": 1
+ "minLength": 1,
+ "maxLength": 100
},
"limit": {
"type": "number",
@@ -1079,6 +1743,7 @@
},
"search": {
"type": "string",
+ "maxLength": 100,
"description": "Search listings by game name"
}
},
@@ -1831,7 +2496,8 @@
]
},
"search": {
- "type": "string"
+ "type": "string",
+ "maxLength": 100
},
"minMemory": {
"type": "number",
@@ -3175,7 +3841,7 @@
"$ref": "#/components/schemas/DeleteMobileAccountSchema"
},
"example": {
- "confirmationText": "example"
+ "confirmationText": "DELETE"
}
}
},
@@ -3308,8 +3974,8 @@
},
"/catalog.getDeviceCompatibility": {
"get": {
- "summary": "Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 10 minutes to reduce server load.",
- "description": "Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 10 minutes to reduce server load.",
+ "summary": "Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 15 minutes to reduce server load.",
+ "description": "Get device compatibility scores by system Returns aggregated compatibility scores (0-100) for each system tested on a device. Scores are calculated from: - Performance ratings from authors - Community votes (Wilson score) - Developer verifications Results are cached for 15 minutes to reduce server load.",
"tags": [
"catalog"
],
@@ -6940,8 +7606,8 @@
},
"/games.batchBySteamAppIds": {
"get": {
- "summary": "Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 5 minutes to optimize repeated queries",
- "description": "Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 5 minutes to optimize repeated queries",
+ "summary": "Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 15 minutes to optimize repeated queries",
+ "description": "Batch lookup games by Steam App IDs Optimized for large batches (up to 1000 Steam App IDs) Returns games with their listings filtered by emulator if specified Results cached for 15 minutes to optimize repeated queries",
"tags": [
"games"
],
@@ -6971,8 +7637,7 @@
"description": "tRPC result wrapper containing the actual response data",
"properties": {
"data": {
- "type": "object",
- "description": "Response data from games.batchBySteamAppIds"
+ "$ref": "#/components/schemas/BatchBySteamAppIdsResponseSchema"
}
}
}
@@ -6987,14 +7652,41 @@
"value": {
"result": {
"data": {
- "message": "Response from games.batchBySteamAppIds",
- "data": {
- "id": "uuid-game",
- "title": "Super Mario Bros",
- "systemId": "uuid-system",
- "imageUrl": "https://example.com/game.jpg",
- "status": "APPROVED"
- }
+ "success": true,
+ "results": [
+ {
+ "game_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+ "steam_app_id": "220",
+ "title": "Half-Life 2",
+ "performance": {
+ "id": 1,
+ "label": "Perfect",
+ "rank": 1,
+ "description": null
+ },
+ "emulator": {
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+ "name": "GameHub",
+ "logo": null
+ },
+ "device": {
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+ "modelName": "Steam Deck",
+ "soc": null
+ },
+ "listing": {
+ "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
+ "notes": "Runs well",
+ "upvoteCount": 4,
+ "downvoteCount": 1,
+ "voteCount": 5,
+ "successRate": 0.8
+ }
+ }
+ ],
+ "totalRequested": 1,
+ "totalFound": 1,
+ "totalNotFound": 0
}
}
}
diff --git a/src/app/admin/components/ApprovalCountBadge.test.tsx b/src/app/admin/components/ApprovalCountBadge.test.tsx
index f59d00230..352b76225 100644
--- a/src/app/admin/components/ApprovalCountBadge.test.tsx
+++ b/src/app/admin/components/ApprovalCountBadge.test.tsx
@@ -1,57 +1,53 @@
import { render, screen } from '@testing-library/react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
-import { api } from '@/lib/api'
import { PERMISSIONS } from '@/utils/permission-system'
import ApprovalCountBadge from './ApprovalCountBadge'
+interface UserQueryResult {
+ data?: {
+ permissions?: string[] | null
+ } | null
+}
+
+interface StatsQueryResult {
+ data?: {
+ pending: number
+ approved: number
+ rejected: number
+ total: number
+ }
+}
+
+const apiMocks = vi.hoisted(() => ({
+ userMeUseQuery: vi.fn<() => UserQueryResult>(),
+ gamesStatsUseQuery: vi.fn<(input?: undefined, options?: unknown) => StatsQueryResult>(),
+ listingsStatsUseQuery: vi.fn<(input?: undefined, options?: unknown) => StatsQueryResult>(),
+ pcListingsStatsUseQuery: vi.fn<(input?: undefined, options?: unknown) => StatsQueryResult>(),
+}))
+
vi.mock('@/lib/api', () => ({
api: {
- users: { me: { useQuery: vi.fn() } },
- games: { stats: { useQuery: vi.fn() } },
- listings: { stats: { useQuery: vi.fn() } },
- pcListings: { stats: { useQuery: vi.fn() } },
+ users: { me: { useQuery: apiMocks.userMeUseQuery } },
+ games: { stats: { useQuery: apiMocks.gamesStatsUseQuery } },
+ listings: { stats: { useQuery: apiMocks.listingsStatsUseQuery } },
+ pcListings: { stats: { useQuery: apiMocks.pcListingsStatsUseQuery } },
},
}))
-const mockUserQuery = vi.mocked(api.users.me.useQuery)
-const mockGamesStatsQuery = vi.mocked(api.games.stats.useQuery)
-const mockListingsStatsQuery = vi.mocked(api.listings.stats.useQuery)
-const mockPcListingsStatsQuery = vi.mocked(api.pcListings.stats.useQuery)
-
describe('ApprovalCountBadge', () => {
beforeEach(() => {
vi.clearAllMocks()
})
it('renders badge when count is available and user has permission', () => {
- mockUserQuery.mockReturnValue({
+ apiMocks.userMeUseQuery.mockReturnValue({
data: { permissions: [PERMISSIONS.VIEW_STATISTICS] },
- isLoading: false,
- isError: false,
- error: null,
- trpc: {},
- } as any)
- mockGamesStatsQuery.mockReturnValue({
+ })
+ apiMocks.gamesStatsUseQuery.mockReturnValue({
data: { pending: 3, approved: 0, rejected: 0, total: 3 },
- isLoading: false,
- isError: false,
- error: null,
- trpc: {},
- } as any)
- mockListingsStatsQuery.mockReturnValue({
- data: undefined,
- isLoading: false,
- isError: false,
- error: null,
- trpc: {},
- } as any)
- mockPcListingsStatsQuery.mockReturnValue({
- data: undefined,
- isLoading: false,
- isError: false,
- error: null,
- trpc: {},
- } as any)
+ })
+ apiMocks.listingsStatsUseQuery.mockReturnValue({})
+ apiMocks.pcListingsStatsUseQuery.mockReturnValue({})
render(