Merge branch 'master' into console-mode

This commit is contained in:
Georges-Antoine Assi
2025-08-29 13:07:05 -05:00
committed by GitHub
24 changed files with 268 additions and 79 deletions

View File

@@ -4,10 +4,10 @@
"service": "romm-dev",
"workspaceFolder": "/app",
"shutdownAction": "stopCompose",
"forwardPorts": [5000, 3000, 3306, 6379],
"forwardPorts": [8443, 3000, 3306, 5000, 6379],
"portsAttributes": {
"5000": {
"label": "Backend API",
"8443": {
"label": "HTTPS Dev Server",
"onAutoForward": "notify"
},
"3000": {
@@ -18,6 +18,10 @@
"label": "MariaDB",
"onAutoForward": "silent"
},
"5000": {
"label": "Backend API",
"onAutoForward": "notify"
},
"6379": {
"label": "Valkey/Redis",
"onAutoForward": "silent"

View File

@@ -27,7 +27,6 @@ cp env.template .env
```dotenv
ROMM_BASE_PATH=/app/romm
GUNICORN_WORKERS=4
DEV_MODE=true
```

View File

@@ -8,6 +8,8 @@ Create Date: 2025-08-22 04:42:22.367888
import sqlalchemy as sa
from alembic import op
from models.firmware import Firmware
from utils.database import is_postgresql
# revision identifiers, used by Alembic.
revision = "0050_firmware_add_is_verified"
@@ -20,16 +22,24 @@ def upgrade() -> None:
with op.batch_alter_table("firmware", schema=None) as batch_op:
batch_op.add_column(sa.Column("is_verified", sa.Boolean(), nullable=True))
# Calculate is_verified for all firmware
from handler.database import db_firmware_handler
from models.firmware import Firmware
# Get all firmware records with their hash information
connection = op.get_bind()
result = connection.execute(
sa.text(
"""
SELECT f.id, p.slug as platform_slug, f.file_name, f.file_size_bytes, f.md5_hash, f.sha1_hash, f.crc_hash
FROM firmware f
JOIN platforms p ON f.platform_id = p.id
"""
)
)
all_firmware = db_firmware_handler.list_firmware()
all_firmware = result.fetchall()
verified_firmware_ids = []
for firmware in all_firmware:
is_verified = Firmware.verify_file_hashes(
platform_slug=firmware.platform.slug,
platform_slug=firmware.platform_slug,
file_name=firmware.file_name,
file_size_bytes=firmware.file_size_bytes,
md5_hash=firmware.md5_hash,
@@ -39,12 +49,34 @@ def upgrade() -> None:
if is_verified:
verified_firmware_ids.append(firmware.id)
op.execute("UPDATE firmware SET is_verified = 0")
# Set all firmware as not verified initially
if is_postgresql(connection):
op.execute(sa.text("UPDATE firmware SET is_verified = FALSE"))
else:
op.execute(sa.text("UPDATE firmware SET is_verified = 0"))
if verified_firmware_ids:
op.execute(
# trunk-ignore(bandit/B608)
f"UPDATE firmware SET is_verified = 1 WHERE id IN ({','.join(map(str, verified_firmware_ids))})"
placeholders = ",".join(
[":id" + str(i) for i in range(len(verified_firmware_ids))]
)
params = {
f"id{i}": verified_firmware_ids[i]
for i in range(len(verified_firmware_ids))
}
if is_postgresql(connection):
op.execute(
sa.text(
# trunk-ignore(bandit/B608)
f"UPDATE firmware SET is_verified = TRUE WHERE id IN ({placeholders})"
).bindparams(**params)
)
else:
op.execute(
sa.text(
# trunk-ignore(bandit/B608)
f"UPDATE firmware SET is_verified = 1 WHERE id IN ({placeholders})"
).bindparams(**params)
)
with op.batch_alter_table("firmware", schema=None) as batch_op:
batch_op.alter_column("is_verified", existing_type=sa.Boolean(), nullable=False)

View File

@@ -123,6 +123,7 @@ OIDC_TLS_CACERTFILE: Final = os.environ.get("OIDC_TLS_CACERTFILE", None)
SCAN_TIMEOUT: Final = int(os.environ.get("SCAN_TIMEOUT", 60 * 60 * 4)) # 4 hours
# TASKS
TASK_TIMEOUT: Final = int(os.environ.get("TASK_TIMEOUT", 60 * 5)) # 5 minutes
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE: Final = str_to_bool(
os.environ.get("ENABLE_RESCAN_ON_FILESYSTEM_CHANGE", "false")
)

View File

@@ -211,6 +211,7 @@ async def _identify_rom(
return scan_stats
# Build rom files object before scanning
log.debug(f"Calculating file hashes for {rom.fs_name}...")
rom_files, rom_crc_c, rom_md5_h, rom_sha1_h, rom_ra_h = (
await fs_rom_handler.get_rom_files(rom)
)
@@ -224,6 +225,7 @@ async def _identify_rom(
}
)
log.debug(f"Scanning {rom.fs_name}...")
scanned_rom = await scan_rom(
scan_type=scan_type,
platform=platform,
@@ -510,7 +512,7 @@ async def scan_platforms(
if len(missed_platforms) > 0:
log.warning(f"{hl('Missing')} platforms from filesystem:")
for p in missed_platforms:
log.warning(f" - {p.slug}")
log.warning(f" - {p.slug} ({p.fs_slug})")
log.info(f"{emoji.EMOJI_CHECK_MARK} Scan completed")
await sm.emit("scan:done", scan_stats.__dict__)

View File

@@ -3,6 +3,7 @@ from datetime import datetime, timezone
from config import (
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
TASK_TIMEOUT,
)
from decorators.auth import protected_route
from endpoints.responses import TaskExecutionResponse, TaskStatusResponse
@@ -144,7 +145,7 @@ async def run_all_tasks(request: Request) -> list[TaskExecutionResponse]:
)
jobs = [
(task_name, low_prio_queue.enqueue(task_instance.run))
(task_name, low_prio_queue.enqueue(task_instance.run, job_timeout=TASK_TIMEOUT))
for task_name, task_instance in runnable_tasks.items()
]
@@ -185,7 +186,7 @@ async def run_single_task(request: Request, task_name: str) -> TaskExecutionResp
detail=f"Task '{task_name}' cannot be run",
)
job = low_prio_queue.enqueue(task_instance.run)
job = low_prio_queue.enqueue(task_instance.run, job_timeout=TASK_TIMEOUT)
return {
"task_name": task_name,

View File

@@ -1,14 +1,17 @@
from typing import Annotated, Any
from typing import Annotated, Any, cast
from decorators.auth import protected_route
from endpoints.forms.identity import UserForm
from endpoints.responses.identity import InviteLinkSchema, UserSchema
from fastapi import Body, Form, HTTPException, Request, status
from fastapi import Body, Form, HTTPException
from fastapi import Path as PathVar
from fastapi import Request, status
from handler.auth import auth_handler
from handler.auth.constants import Scope
from handler.database import db_user_handler
from handler.filesystem import fs_asset_handler
from handler.metadata import meta_ra_handler
from handler.metadata.ra_handler import RAUserProgression
from logger.logger import log
from models.user import Role, User
from utils.router import APIRouter
@@ -371,33 +374,42 @@ async def delete_user(request: Request, id: int) -> None:
@protected_route(
router.post, "/{id}/ra/refresh", [Scope.ME_WRITE], status_code=status.HTTP_200_OK
router.post,
"/{id}/ra/refresh",
[Scope.ME_WRITE],
status_code=status.HTTP_200_OK,
summary="Refresh RetroAchievements",
responses={status.HTTP_404_NOT_FOUND: {}},
)
async def refresh_retro_achievements(request: Request, id: int) -> None:
"""Refresh RetroAchievements data for a user.
Args:
request (Request): FastAPI Request object
id (int): User ID
Raises:
HTTPException: User not found or no RetroAchievements username set
Returns:
None: Returns 200 OK status
"""
async def refresh_retro_achievements(
request: Request,
id: Annotated[int, PathVar(description="User internal id.", ge=1)],
incremental: Annotated[
bool,
Body(
description="Whether to only retrieve RetroAchievements progression incrementally.",
embed=True,
),
] = False,
) -> None:
"""Refresh RetroAchievements progression data for a user."""
user = db_user_handler.get_user(id)
if user and user.ra_username:
user_progression = await meta_ra_handler.get_user_progression(user.ra_username)
db_user_handler.update_user(
id,
{
"ra_progression": user_progression,
},
if not user or not user.ra_username:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have a RetroAchievements username set",
)
return None
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not have a RetroAchievements username set",
user_progression = await meta_ra_handler.get_user_progression(
user.ra_username,
current_progression=(
cast(RAUserProgression | None, user.ra_progression) if incremental else None
),
)
db_user_handler.update_user(
id,
{
"ra_progression": user_progression,
},
)
return None

View File

@@ -69,6 +69,7 @@ class RAUserGameProgression(TypedDict):
max_possible: int | None
num_awarded: int | None
num_awarded_hardcore: int | None
most_recent_awarded_date: NotRequired[str | None]
earned_achievements: list[EarnedAchievement]
@@ -263,11 +264,40 @@ class RAHandler(MetadataHandler):
except KeyError:
return RAGameRom(ra_id=None)
async def get_user_progression(self, username: str) -> RAUserProgression:
async def get_user_progression(
self,
username: str,
current_progression: RAUserProgression | None = None,
) -> RAUserProgression:
"""Retrieves the user's RetroAchievements progression.
If `current_progression` is provided, it will only incrementally update the
progression based on new achievements since the last check.
"""
game_progressions: list[RAUserGameProgression] = []
current_progression_by_game_id: dict[int | None, RAUserGameProgression] = {}
if current_progression:
current_progression_by_game_id = {
p["rom_ra_id"]: p for p in current_progression.get("results", [])
}
async for rom in self.ra_service.iter_user_completion_progress(username):
rom_game_id = rom.get("GameID")
# If we have current progression data, and number of awarded achievements and most
# recent awarded date match, then we can skip fetching progression details.
game_current_progression = current_progression_by_game_id.get(rom_game_id)
if (
game_current_progression
and rom["NumAwarded"] == game_current_progression.get("num_awarded")
and rom["NumAwardedHardcore"]
== game_current_progression.get("num_awarded_hardcore")
and rom["MostRecentAwardedDate"]
== game_current_progression.get("most_recent_awarded_date")
):
game_progressions.append(game_current_progression)
continue
earned_achievements: list[EarnedAchievement] = []
if rom_game_id:
result = await self.ra_service.get_user_game_progress(
@@ -293,6 +323,7 @@ class RAHandler(MetadataHandler):
max_possible=rom.get("MaxPossible", None),
num_awarded=rom.get("NumAwarded", None),
num_awarded_hardcore=rom.get("NumAwardedHardcore", None),
most_recent_awarded_date=rom.get("MostRecentAwardedDate", None),
earned_achievements=earned_achievements,
)
)

View File

@@ -12,6 +12,10 @@ class SocketHandler:
logger=False,
engineio_logger=False,
client_manager=socketio.AsyncRedisManager(str(REDIS_URL)),
ping_timeout=60,
ping_interval=25,
max_http_buffer_size=1e6, # 1MB
cors_credentials=True,
)
self.socket_app = socketio.ASGIApp(

View File

@@ -2,6 +2,7 @@ from abc import ABC, abstractmethod
from typing import Any
import httpx
from config import TASK_TIMEOUT
from exceptions.task_exceptions import SchedulerException
from handler.redis_handler import low_prio_queue
from logger.logger import log
@@ -86,6 +87,7 @@ class PeriodicTask(Task, ABC):
self.cron_string,
func=self.func,
repeat=None,
timeout=TASK_TIMEOUT,
)
return None

View File

@@ -132,7 +132,7 @@ class TestPeriodicTask:
result = task.schedule()
mock_cron.assert_called_once_with(
"0 0 * * *", func="test.function", repeat=None
"0 0 * * *", func="test.function", repeat=None, timeout=5 * 60
)
assert result == mock_job

View File

@@ -12,6 +12,7 @@ from config import (
LAUNCHBOX_API_ENABLED,
LIBRARY_BASE_PATH,
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
SCAN_TIMEOUT,
SENTRY_DSN,
)
from config.config_manager import config_manager as cm
@@ -133,6 +134,7 @@ def process_changes(changes: Sequence[Change]) -> None:
[],
scan_type=ScanType.UNIDENTIFIED,
metadata_sources=metadata_sources,
timeout=SCAN_TIMEOUT,
)
return
@@ -155,6 +157,7 @@ def process_changes(changes: Sequence[Change]) -> None:
[db_platform.id],
scan_type=ScanType.QUICK,
metadata_sources=metadata_sources,
timeout=SCAN_TIMEOUT,
)

View File

@@ -13,7 +13,8 @@ services:
- ROMM_BASE_PATH=/app/romm
ports:
- "3000:3000" # Frontend dev server
- "${DEV_PORT:-5050}:5050" # Backend API
- "8443:8443" # HTTPS dev server
- "${DEV_PORT:-5000}:5000" # Backend API
volumes:
- ./backend:/app/backend
- /app/backend/romm_mock # Empty directory
@@ -21,6 +22,7 @@ services:
- /app/frontend/node_modules # Empty directory
- /app/frontend/dist # Empty directory
- ./romm_mock:/app/romm
- ~/.vite-plugin-mkcert:/app/.vite-plugin-mkcert
depends_on:
- romm-db-dev
- romm-valkey-dev

View File

@@ -36,4 +36,3 @@ args=(sys.stdout,)
[formatter_gunicorn_format]
format=INFO: [RomM][gunicorn][%(asctime)s] %(message)s
datefmt=%Y-%m-%d %H:%M:%S

View File

@@ -16,10 +16,6 @@ ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB="${ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB
# if REDIS_HOST is set, we assume that an external redis is used
REDIS_HOST="${REDIS_HOST:=""}"
# set DEFAULT_WEB_CONCURRENCY to 1 if not set by docker env to reduce resource usage
# (since backend is almost 100% async this won't block anything)
DEFAULT_WEB_CONCURRENCY=1
# logger colors
RED='\033[0;31m'
LIGHTMAGENTA='\033[0;95m'
@@ -97,6 +93,9 @@ start_bin_gunicorn() {
# commands to start our main application and store its PID to check for crashes
info_log "Starting backend"
export PYTHONUNBUFFERED=1
export PYTHONDONTWRITEBYTECODE=1
opentelemetry-instrument \
--service_name "${OTEL_SERVICE_NAME_PREFIX-}api" \
gunicorn \
@@ -105,7 +104,12 @@ start_bin_gunicorn() {
--pid=/tmp/gunicorn.pid \
--forwarded-allow-ips="*" \
--worker-class uvicorn_worker.UvicornWorker \
--workers "${WEB_CONCURRENCY:-${DEFAULT_WEB_CONCURRENCY:-1}}" \
--workers "${WEB_SERVER_CONCURRENCY:-1}" \
--timeout "${WEB_SERVER_TIMEOUT:-300}" \
--keep-alive "${WEB_SERVER_KEEPALIVE:-2}" \
--max-requests "${WEB_SERVER_MAX_REQUESTS:-1000}" \
--max-requests-jitter "${WEB_SERVER_MAX_REQUESTS_JITTER:-100}" \
--worker-connections "${WEB_SERVER_WORKER_CONNECTIONS:-1000}" \
--error-logfile - \
--log-config /etc/gunicorn/logging.conf \
main:app &

View File

@@ -1,11 +1,7 @@
ROMM_BASE_PATH=/path/to/romm_mock
DEV_MODE=true
DEV_HTTPS=false
KIOSK_MODE=false
VITE_ALLOWED_HOSTS=false
# Gunicorn (optional)
# Workers -> (2 × CPU cores) + 1
WEB_CONCURRENCY=4
# IGDB credentials
IGDB_CLIENT_ID=
@@ -76,7 +72,8 @@ OIDC_SERVER_APPLICATION_URL=
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE=true
RESCAN_ON_FILESYSTEM_CHANGE_DELAY=5
# Periodic Tasks (optional)
# Tasks (optional)
TASK_TIMEOUT=300
ENABLE_SCHEDULED_RESCAN=true
SCHEDULED_RESCAN_CRON=0 3 * * *
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB=true
@@ -93,3 +90,12 @@ YOUTUBE_BASE_URL=https://www.youtube.com
# Logging
LOGLEVEL=DEBUG
# Web server (optional)
# Workers -> (2 × CPU cores) + 1
WEB_SERVER_CONCURRENCY=2
WEB_SERVER_TIMEOUT=300
WEB_SERVER_KEEPALIVE=2
WEB_SERVER_MAX_REQUESTS=1000
WEB_SERVER_MAX_REQUESTS_JITTER=100
WEB_SERVER_WORKER_CONNECTIONS=1000

View File

@@ -49,6 +49,7 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.28.0",
"vite": "^6.3.5",
"vite-plugin-mkcert": "^1.17.8",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-vuetify": "^2.0.4",
"vue-tsc": "^2.2.8"
@@ -1923,6 +1924,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"aix"
@@ -1938,6 +1940,7 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -1953,6 +1956,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -1968,6 +1972,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"android"
@@ -1983,6 +1988,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@@ -1998,6 +2004,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"darwin"
@@ -2013,6 +2020,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@@ -2028,6 +2036,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"freebsd"
@@ -2043,6 +2052,7 @@
"cpu": [
"arm"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2058,6 +2068,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2073,6 +2084,7 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2088,6 +2100,7 @@
"cpu": [
"loong64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2103,6 +2116,7 @@
"cpu": [
"mips64el"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2118,6 +2132,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2133,6 +2148,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2148,6 +2164,7 @@
"cpu": [
"s390x"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2163,6 +2180,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"linux"
@@ -2178,6 +2196,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@@ -2193,6 +2212,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"netbsd"
@@ -2208,6 +2228,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@@ -2223,6 +2244,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"openbsd"
@@ -2238,6 +2260,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"sunos"
@@ -2253,6 +2276,7 @@
"cpu": [
"arm64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -2268,6 +2292,7 @@
"cpu": [
"ia32"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -2283,6 +2308,7 @@
"cpu": [
"x64"
],
"dev": true,
"optional": true,
"os": [
"win32"
@@ -2581,7 +2607,7 @@
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
@@ -2606,7 +2632,7 @@
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"devOptional": true,
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -2615,7 +2641,7 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"devOptional": true,
"dev": true,
"engines": {
"node": ">=6.0.0"
}
@@ -2624,7 +2650,7 @@
"version": "0.3.6",
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25"
@@ -2639,7 +2665,7 @@
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
@@ -2946,6 +2972,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2959,6 +2986,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2972,6 +3000,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2984,6 +3013,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2997,6 +3027,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3010,6 +3041,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3023,6 +3055,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3036,6 +3069,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3049,6 +3083,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3062,6 +3097,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3075,6 +3111,7 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3088,6 +3125,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3101,6 +3139,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3114,6 +3153,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3127,6 +3167,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3140,6 +3181,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3153,6 +3195,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3166,6 +3209,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3179,6 +3223,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3192,6 +3237,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -3571,7 +3617,7 @@
"version": "22.13.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.20.0"
@@ -4130,7 +4176,7 @@
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"dev": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -4418,7 +4464,7 @@
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"devOptional": true
"dev": true
},
"node_modules/call-bind": {
"version": "1.0.8",
@@ -5639,6 +5685,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"os": [
@@ -8134,7 +8181,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"devOptional": true,
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@@ -8151,7 +8198,7 @@
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"devOptional": true,
"dev": true,
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
@@ -8433,7 +8480,7 @@
"version": "5.37.0",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz",
"integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==",
"devOptional": true,
"dev": true,
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2",
@@ -8451,7 +8498,7 @@
"version": "2.20.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"devOptional": true
"dev": true
},
"node_modules/tinyglobby": {
"version": "0.2.14",
@@ -8707,7 +8754,7 @@
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"devOptional": true
"dev": true
},
"node_modules/unicode-canonical-property-names-ecmascript": {
"version": "2.0.1",
@@ -8907,6 +8954,23 @@
}
}
},
"node_modules/vite-plugin-mkcert": {
"version": "1.17.8",
"resolved": "https://registry.npmjs.org/vite-plugin-mkcert/-/vite-plugin-mkcert-1.17.8.tgz",
"integrity": "sha512-S+4tNEyGqdZQ3RLAG54ETeO2qyURHWrVjUWKYikLAbmhh/iJ+36gDEja4OWwFyXNuvyXcZwNt5TZZR9itPeG5Q==",
"dev": true,
"dependencies": {
"axios": "^1.8.3",
"debug": "^4.4.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=v16.7.0"
},
"peerDependencies": {
"vite": ">=3"
}
},
"node_modules/vite-plugin-pwa": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-0.21.1.tgz",

View File

@@ -67,6 +67,7 @@
"typescript": "^5.7.3",
"typescript-eslint": "^8.28.0",
"vite": "^6.3.5",
"vite-plugin-mkcert": "^1.17.8",
"vite-plugin-pwa": "^0.21.1",
"vite-plugin-vuetify": "^2.0.4",
"vue-tsc": "^2.2.8"

View File

@@ -8,6 +8,7 @@ export type RAUserGameProgression = {
max_possible: (number | null);
num_awarded: (number | null);
num_awarded_hardcore: (number | null);
most_recent_awarded_date?: (string | null);
earned_achievements: Array<EarnedAchievement>;
};

View File

@@ -19,7 +19,7 @@ defineProps<{ title: string }>();
:lg="views[0]['size-lg']"
:xl="views[0]['size-xl']"
>
<skeleton type="image" />
<skeleton />
</v-col>
</v-row>
</template>

View File

@@ -21,7 +21,7 @@ const rules = [
},
];
async function refreshRetroAchievements() {
async function refreshRetroAchievements(incremental = false) {
if (!auth.user) return;
syncing.value = true;
@@ -29,6 +29,7 @@ async function refreshRetroAchievements() {
await userApi
.refreshRetroAchievements({
id: auth.user.id,
incremental,
})
.then(() => {
emitter?.emit("snackbarShow", {
@@ -113,7 +114,7 @@ watch(
:disabled="syncing"
:loading="syncing"
class="ml-4 text-accent bg-toplayer"
@click="refreshRetroAchievements"
@click="refreshRetroAchievements(true)"
>
<template #loader>
<v-progress-circular

View File

@@ -109,6 +109,7 @@ const computedAspectRatio = computed(() => {
height: 24px;
margin: 4px;
flex: 1;
max-width: unset;
}
.card-skeleton.show-action-bar {

View File

@@ -80,8 +80,14 @@ async function deleteUser(user: User) {
return api.delete(`/users/${user.id}`);
}
async function refreshRetroAchievements({ id }: { id: number }) {
return api.post(`/users/${id}/ra/refresh`);
async function refreshRetroAchievements({
id,
incremental = false,
}: {
id: number;
incremental?: boolean;
}) {
return api.post(`/users/${id}/ra/refresh`, { incremental });
}
export default {

View File

@@ -2,6 +2,7 @@
import vue from "@vitejs/plugin-vue";
import { VitePWA } from "vite-plugin-pwa";
import vuetify, { transformAssetUrls } from "vite-plugin-vuetify";
import mkcert from "vite-plugin-mkcert";
// Utilities
import { URL, fileURLToPath } from "node:url";
@@ -18,7 +19,6 @@ export default defineConfig(({ mode }) => {
};
const backendPort = env.DEV_PORT ?? "5000";
const allowedHosts = env.VITE_ALLOWED_HOSTS == "true" ? true : false;
return {
build: {
@@ -49,6 +49,11 @@ export default defineConfig(({ mode }) => {
type: "module",
},
}),
env.DEV_HTTPS &&
mkcert({
savePath: "/app/.vite-plugin-mkcert",
hosts: ["localhost", "127.0.0.1", "romm.dev"],
}),
],
define: {
"process.env": {},
@@ -78,8 +83,16 @@ export default defineConfig(({ mode }) => {
rewrite: (path) => path.replace(/^\/openapi.json/, "/openapi.json"),
},
},
port: 3000,
allowedHosts: allowedHosts,
port: env.DEV_HTTPS ? 8443 : 3000,
allowedHosts: ["localhost", "127.0.0.1", "romm.dev"],
...(env.DEV_HTTPS
? {
https: {
cert: "/app/.vite-plugin-mkcert/dev.pem",
key: "/app/.vite-plugin-mkcert/dev-key.pem",
},
}
: {}),
},
};
});