mirror of
https://github.com/rommapp/romm.git
synced 2026-02-18 00:27:41 +01:00
Merge branch 'master' into console-mode
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -27,7 +27,6 @@ cp env.template .env
|
||||
|
||||
```dotenv
|
||||
ROMM_BASE_PATH=/app/romm
|
||||
GUNICORN_WORKERS=4
|
||||
DEV_MODE=true
|
||||
```
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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__)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 &
|
||||
|
||||
18
env.template
18
env.template
@@ -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
|
||||
|
||||
90
frontend/package-lock.json
generated
90
frontend/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -109,6 +109,7 @@ const computedAspectRatio = computed(() => {
|
||||
height: 24px;
|
||||
margin: 4px;
|
||||
flex: 1;
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.card-skeleton.show-action-bar {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user