feat: Added clean resources task + revamped the whole task system

This commit is contained in:
zurdi
2025-07-22 16:31:36 +00:00
parent f3c0e484f5
commit 7aeccb5468
25 changed files with 514 additions and 261 deletions

View File

@@ -2,19 +2,11 @@ from config import (
DISABLE_EMULATOR_JS,
DISABLE_RUFFLE_RS,
DISABLE_USERPASS_LOGIN,
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
ENABLE_SCHEDULED_RESCAN,
ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB,
HASHEOUS_API_ENABLED,
LAUNCHBOX_API_ENABLED,
OIDC_ENABLED,
OIDC_PROVIDER,
PLAYMATCH_API_ENABLED,
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
SCHEDULED_RESCAN_CRON,
SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
TGDB_API_ENABLED,
UPLOAD_TIMEOUT,
)
@@ -68,31 +60,6 @@ async def heartbeat() -> HeartbeatResponse:
"FILESYSTEM": {
"FS_PLATFORMS": await fs_platform_handler.get_platforms(),
},
"WATCHER": {
"ENABLED": ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
"TITLE": "Rescan on filesystem change",
"MESSAGE": f"Runs a scan when a change is detected in the library path, with a {RESCAN_ON_FILESYSTEM_CHANGE_DELAY} minute delay",
},
"SCHEDULER": {
"RESCAN": {
"ENABLED": ENABLE_SCHEDULED_RESCAN,
"CRON": SCHEDULED_RESCAN_CRON,
"TITLE": "Scheduled rescan",
"MESSAGE": "Rescans the entire library",
},
"SWITCH_TITLEDB": {
"ENABLED": ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB, # noqa
"CRON": SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
"TITLE": "Scheduled Switch TitleDB update",
"MESSAGE": "Updates the Nintendo Switch TitleDB file",
},
"LAUNCHBOX_METADATA": {
"ENABLED": ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
"CRON": SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
"TITLE": "Scheduled LaunchBox metadata update",
"MESSAGE": "Updates the LaunchBox metadata store",
},
},
"EMULATION": {
"DISABLE_EMULATOR_JS": DISABLE_EMULATOR_JS,
"DISABLE_RUFFLE_RS": DISABLE_RUFFLE_RS,

View File

@@ -6,22 +6,6 @@ class SystemDict(TypedDict):
SHOW_SETUP_WIZARD: bool
class WatcherDict(TypedDict):
ENABLED: bool
TITLE: str
MESSAGE: str
class TaskDict(WatcherDict):
CRON: str
class SchedulerDict(TypedDict):
RESCAN: TaskDict
SWITCH_TITLEDB: TaskDict
LAUNCHBOX_METADATA: TaskDict
class MetadataSourcesDict(TypedDict):
ANY_SOURCE_ENABLED: bool
IGDB_API_ENABLED: bool
@@ -56,8 +40,6 @@ class OIDCDict(TypedDict):
class HeartbeatResponse(TypedDict):
SYSTEM: SystemDict
WATCHER: WatcherDict
SCHEDULER: SchedulerDict
METADATA_SOURCES: MetadataSourcesDict
FILESYSTEM: FilesystemDict
EMULATION: EmulationDict

View File

@@ -0,0 +1,14 @@
from typing import Dict, List, TypedDict
class TaskInfoDict(TypedDict):
name: str
manual_run: bool
title: str
description: str
enabled: bool
cron_string: str
# Use a more flexible type for grouped tasks
GroupedTasksDict = Dict[str, List[TaskInfoDict]]

View File

@@ -1,46 +1,189 @@
import importlib
from pathlib import Path
from typing import Any, Dict
from config import (
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
)
from decorators.auth import protected_route
from endpoints.responses import MessageResponse
from fastapi import Request
from endpoints.responses.tasks import GroupedTasksDict, TaskInfoDict
from fastapi import HTTPException, Request
from handler.auth.constants import Scope
from tasks.update_launchbox_metadata import update_launchbox_metadata_task
from tasks.update_switch_titledb import update_switch_titledb_task
from logger.logger import log
from utils.router import APIRouter
TASK_TYPES = ["scheduled", "manual"]
router = APIRouter(
prefix="/tasks",
tags=["tasks"],
)
def _get_available_tasks() -> Dict[str, Any]:
"""Automatically discover all available tasks by scanning the tasks directory."""
tasks = {}
for task_type in TASK_TYPES:
tasks_dir = Path(__file__).parent.parent / "tasks" / task_type
for task_file in tasks_dir.glob("*.py"):
module_name = f"tasks.{task_type}.{task_file.stem}"
try:
module = importlib.import_module(module_name)
# Look for task instances (variables ending with _task)
for attr_name in dir(module):
if attr_name.endswith("_task") and not attr_name.startswith("_"):
task_instance = getattr(module, attr_name)
# Verify it has a run method
if hasattr(task_instance, "run") and callable(
task_instance.run
):
# Use the task name without the _task suffix as the key
task_key = attr_name.replace("_task", "")
tasks[task_key] = task_instance
except ImportError as e:
log.error(f"Warning: Could not import task module {module_name}: {e}")
return tasks
@protected_route(router.get, "", [Scope.TASKS_RUN])
async def list_tasks(request: Request) -> GroupedTasksDict:
"""List all available tasks grouped by task type.
Args:
request (Request): FastAPI Request object
Returns:
Dictionary with tasks grouped by their type (scheduled, manual, watcher)
"""
tasks = _get_available_tasks()
# Initialize the grouped tasks dictionary
grouped_tasks: GroupedTasksDict = {}
# Group tasks by type
for task_type in TASK_TYPES:
task_list: list[TaskInfoDict] = []
tasks_dir = Path(__file__).parent.parent / "tasks" / task_type
for name, instance in tasks.items():
# Check if this task belongs to the current type by checking if it exists in the type directory
task_file_path = tasks_dir / f"{name}.py"
if task_file_path.exists():
manual_run = getattr(instance, "manual_run", False)
title = getattr(instance, "title", name.replace("_", " ").title())
description = getattr(
instance, "description", "No description available"
)
enabled = getattr(instance, "enabled", False)
task_info: TaskInfoDict = {
"name": name,
"manual_run": manual_run,
"title": title,
"description": description,
"enabled": enabled,
"cron_string": getattr(instance, "cron_string", ""),
}
task_list.append(task_info)
if task_list:
grouped_tasks[task_type] = task_list
# Add the adhoc watcher task
watcher_task: TaskInfoDict = {
"name": "filesystem_watcher",
"manual_run": False,
"title": "Rescan on filesystem change",
"description": f"Runs a scan when a change is detected in the library path, with a {RESCAN_ON_FILESYSTEM_CHANGE_DELAY} minute delay",
"enabled": ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
"cron_string": "",
}
grouped_tasks["watcher"] = [watcher_task]
return grouped_tasks
@protected_route(router.post, "/run", [Scope.TASKS_RUN])
async def run_tasks(request: Request) -> MessageResponse:
"""Run all tasks endpoint
async def run_all_tasks(request: Request) -> MessageResponse:
"""Run all runnable tasks endpoint
Args:
request (Request): Fastapi Request object
request (Request): FastAPI Request object
Returns:
RunTasksResponse: Standard message response
MessageResponse: Standard message response
"""
tasks = _get_available_tasks()
await update_switch_titledb_task.run()
await update_launchbox_metadata_task.run()
return {"msg": "All tasks ran successfully!"}
if not tasks:
return {"msg": "No tasks available to run"}
@protected_route(router.post, "/{task}/run", [Scope.TASKS_RUN])
async def run_task(request: Request, task: str) -> MessageResponse:
"""Run single tasks endpoint
Args:
request (Request): Fastapi Request object
Returns:
RunTasksResponse: Standard message response
"""
tasks = {
"switch_titledb": update_switch_titledb_task,
"launchbox_metadata": update_launchbox_metadata_task,
# Filter only runnable tasks
runnable_tasks = {
name: instance
for name, instance in tasks.items()
if getattr(instance, "manual_run", False)
}
await tasks[task].run()
return {"msg": f"Task {task} run successfully!"}
if not runnable_tasks:
return {"msg": "No runnable tasks available to run"}
failed_tasks = []
successful_tasks = []
for task_name, task_instance in runnable_tasks.items():
try:
await task_instance.run()
successful_tasks.append(task_name)
except Exception as e:
failed_tasks.append(f"{task_name}: {str(e)}")
if failed_tasks:
return {
"msg": f"Some tasks failed. Successful: {', '.join(successful_tasks)}. Failed: {', '.join(failed_tasks)}"
}
return {
"msg": f"All {len(successful_tasks)} triggerable tasks ran successfully: {', '.join(successful_tasks)}"
}
@protected_route(router.post, "/run/{task_name}", [Scope.TASKS_RUN])
async def run_single_task(request: Request, task_name: str) -> MessageResponse:
"""Run a single task endpoint.
Args:
request (Request): FastAPI Request object
task_name (str): Name of the task to run
Returns:
MessageResponse: Standard message response
"""
tasks = _get_available_tasks()
if task_name not in tasks:
available_tasks = list(tasks.keys())
raise HTTPException(
status_code=404,
detail=f"Task '{task_name}' not found. Available tasks: {', '.join(available_tasks)}",
)
task_instance = tasks[task_name]
# Check if task is triggerable (manual_run = True)
if not getattr(task_instance, "manual_run", False):
raise HTTPException(
status_code=400,
detail=f"Task '{task_name}' is not triggerable manually.",
)
try:
await task_instance.run()
return {"msg": f"Task '{task_name}' ran successfully!"}
except Exception as e:
raise HTTPException(
status_code=500, detail=f"Task '{task_name}' failed: {str(e)}"
) from e

View File

@@ -8,7 +8,7 @@ from typing import Final, NotRequired, TypedDict
from handler.redis_handler import async_cache, sync_cache
from logger.logger import log
from tasks.update_switch_titledb import (
from tasks.scheduled.update_switch_titledb import (
SWITCH_PRODUCT_ID_KEY,
SWITCH_TITLEDB_INDEX_KEY,
update_switch_titledb_task,

View File

@@ -6,7 +6,7 @@ import pydash
from config import LAUNCHBOX_API_ENABLED, str_to_bool
from handler.redis_handler import async_cache
from logger.logger import log
from tasks.update_launchbox_metadata import ( # LAUNCHBOX_MAME_KEY,
from tasks.scheduled.update_launchbox_metadata import ( # LAUNCHBOX_MAME_KEY,
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
LAUNCHBOX_METADATA_DATABASE_ID_KEY,
LAUNCHBOX_METADATA_IMAGE_KEY,

View File

@@ -6,10 +6,10 @@ from config import (
SENTRY_DSN,
)
from logger.logger import log
from tasks.scan_library import scan_library_task
from tasks.scheduled.scan_library import scan_library_task
from tasks.scheduled.update_launchbox_metadata import update_launchbox_metadata_task
from tasks.scheduled.update_switch_titledb import update_switch_titledb_task
from tasks.tasks import tasks_scheduler
from tasks.update_launchbox_metadata import update_launchbox_metadata_task
from tasks.update_switch_titledb import update_switch_titledb_task
from utils import get_version
sentry_sdk.init(dsn=SENTRY_DSN, release=f"romm@{get_version()}")

View File

@@ -0,0 +1,82 @@
import os
import shutil
from config import RESOURCES_BASE_PATH
from handler.database import db_platform_handler, db_rom_handler
from logger.logger import log
from utils.context import initialize_context
class CleanupOrphanedResourcesTask:
def __init__(self):
self.manual_run = True
self.title = "Cleanup orphaned resources"
self.description = "cleanup orphaned resources"
self.enabled = True
@initialize_context()
async def run(self) -> None:
"""Clean up orphaned resources."""
log.info(f"Starting {self.title} task...")
removed_count = 0
roms_path = os.path.join(RESOURCES_BASE_PATH, "roms")
if not os.path.exists(RESOURCES_BASE_PATH) or not os.path.exists(roms_path):
log.info("Resources path does not exist, skipping cleanup")
return
existing_platforms = {
str(platform.id) for platform in db_platform_handler.get_platforms()
}
log.debug(f"Found {len(existing_platforms)} platforms in database")
for platform_dir in os.listdir(roms_path):
platform_path = os.path.join(roms_path, platform_dir)
if not os.path.isdir(platform_path):
continue
# Check if platform exists in database
if platform_dir not in existing_platforms:
try:
# Remove entire platform directory if platform doesn't exist
shutil.rmtree(platform_path)
removed_count += 1
log.info(f"Removed orphaned platform directory: {platform_dir}")
except Exception as e:
log.error(
f"Failed to remove platform directory {platform_dir}: {e}"
)
continue
platform_id = int(platform_dir)
existing_roms = {
str(rom.id)
for rom in db_rom_handler.get_roms_scalar(platform_id=platform_id)
}
log.debug(f"Found {len(existing_roms)} ROMs for platform {platform_id}")
for rom_dir in os.listdir(platform_path):
rom_path = os.path.join(platform_path, rom_dir)
if not os.path.isdir(rom_path):
continue
# Check if ROM exists in database
if rom_dir not in existing_roms:
try:
# Remove ROM directory if ROM doesn't exist
shutil.rmtree(rom_path)
removed_count += 1
log.info(
f"Removed orphaned ROM directory: {platform_dir}/{rom_dir}"
)
except Exception as e:
log.error(
f"Failed to remove ROM directory {platform_dir}/{rom_dir}: {e}"
)
log.info(f"Removed {removed_count} orphaned resource directories")
log.info("Cleanup of orphaned resources completed!")
cleanup_orphaned_resources_task = CleanupOrphanedResourcesTask()

View File

@@ -19,10 +19,12 @@ class ScanLibraryTask(PeriodicTask):
def __init__(self):
super().__init__(
func="tasks.scan_library.scan_library_task.run",
description="library scan",
description="Rescans the entire library",
enabled=ENABLE_SCHEDULED_RESCAN,
cron_string=SCHEDULED_RESCAN_CRON,
)
self.manual_run = False
self.title = "Scheduled rescan"
async def run(self):
if not ENABLE_SCHEDULED_RESCAN:

View File

@@ -27,11 +27,13 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
def __init__(self):
super().__init__(
func="tasks.update_launchbox_metadata.update_launchbox_metadata_task.run",
description="launchbox metadata update",
description="Updates the LaunchBox metadata store",
enabled=ENABLE_SCHEDULED_UPDATE_LAUNCHBOX_METADATA,
cron_string=SCHEDULED_UPDATE_LAUNCHBOX_METADATA_CRON,
url="https://gamesdb.launchbox-app.com/Metadata.zip",
)
self.manual_run = True
self.title = "Scheduled LaunchBox metadata update"
@initialize_context()
async def run(self, force: bool = False) -> None:

View File

@@ -19,11 +19,13 @@ class UpdateSwitchTitleDBTask(RemoteFilePullTask):
def __init__(self):
super().__init__(
func="tasks.update_switch_titledb.update_switch_titledb_task.run",
description="switch titledb update",
description="Updates the Nintendo Switch TitleDB file",
enabled=ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB,
cron_string=SCHEDULED_UPDATE_SWITCH_TITLEDB_CRON,
url="https://raw.githubusercontent.com/blawar/titledb/master/US.en.json",
)
self.manual_run = True
self.title = "Scheduled Switch TitleDB update"
@initialize_context()
async def run(self, force: bool = False) -> None:

View File

@@ -2,7 +2,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from handler.scan_handler import MetadataSource, ScanType
from tasks.scan_library import ScanLibraryTask, scan_library_task
from tasks.scheduled.scan_library import ScanLibraryTask, scan_library_task
class TestScanLibraryTask:

View File

@@ -3,8 +3,7 @@ from unittest.mock import AsyncMock, patch
import anyio
import pytest
from tasks.tasks import RemoteFilePullTask
from tasks.update_launchbox_metadata import (
from tasks.scheduled.update_launchbox_metadata import (
LAUNCHBOX_FILES_KEY,
LAUNCHBOX_MAME_KEY,
LAUNCHBOX_METADATA_ALTERNATE_NAME_KEY,
@@ -15,6 +14,7 @@ from tasks.update_launchbox_metadata import (
UpdateLaunchboxMetadataTask,
update_launchbox_metadata_task,
)
from tasks.tasks import RemoteFilePullTask
@pytest.fixture

View File

@@ -2,13 +2,13 @@ import json
from unittest.mock import AsyncMock, patch
import pytest
from tasks.tasks import RemoteFilePullTask
from tasks.update_switch_titledb import (
from tasks.scheduled.update_switch_titledb import (
SWITCH_PRODUCT_ID_KEY,
SWITCH_TITLEDB_INDEX_KEY,
UpdateSwitchTitleDBTask,
update_switch_titledb_task,
)
from tasks.tasks import RemoteFilePullTask
class TestUpdateSwitchTitleDBTask:

View File

@@ -54,7 +54,6 @@ export type { RomSSMetadata } from './models/RomSSMetadata';
export type { RomUserSchema } from './models/RomUserSchema';
export type { RomUserStatus } from './models/RomUserStatus';
export type { SaveSchema } from './models/SaveSchema';
export type { SchedulerDict } from './models/SchedulerDict';
export type { ScreenshotSchema } from './models/ScreenshotSchema';
export type { SearchCoverSchema } from './models/SearchCoverSchema';
export type { SearchRomSchema } from './models/SearchRomSchema';
@@ -63,7 +62,7 @@ export type { SimpleRomSchema } from './models/SimpleRomSchema';
export type { StateSchema } from './models/StateSchema';
export type { StatsReturn } from './models/StatsReturn';
export type { SystemDict } from './models/SystemDict';
export type { TaskDict } from './models/TaskDict';
export type { TaskInfoDict } from './models/TaskInfoDict';
export type { TinfoilFeedFileSchema } from './models/TinfoilFeedFileSchema';
export type { TinfoilFeedSchema } from './models/TinfoilFeedSchema';
export type { TinfoilFeedTitleDBSchema } from './models/TinfoilFeedTitleDBSchema';
@@ -73,7 +72,6 @@ export type { UserNotesSchema } from './models/UserNotesSchema';
export type { UserSchema } from './models/UserSchema';
export type { ValidationError } from './models/ValidationError';
export type { VirtualCollectionSchema } from './models/VirtualCollectionSchema';
export type { WatcherDict } from './models/WatcherDict';
export type { WebrcadeFeedCategorySchema } from './models/WebrcadeFeedCategorySchema';
export type { WebrcadeFeedItemPropsSchema } from './models/WebrcadeFeedItemPropsSchema';
export type { WebrcadeFeedItemSchema } from './models/WebrcadeFeedItemSchema';

View File

@@ -7,13 +7,9 @@ import type { FilesystemDict } from './FilesystemDict';
import type { FrontendDict } from './FrontendDict';
import type { MetadataSourcesDict } from './MetadataSourcesDict';
import type { OIDCDict } from './OIDCDict';
import type { SchedulerDict } from './SchedulerDict';
import type { SystemDict } from './SystemDict';
import type { WatcherDict } from './WatcherDict';
export type HeartbeatResponse = {
SYSTEM: SystemDict;
WATCHER: WatcherDict;
SCHEDULER: SchedulerDict;
METADATA_SOURCES: MetadataSourcesDict;
FILESYSTEM: FilesystemDict;
EMULATION: EmulationDict;

View File

@@ -1,11 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { TaskDict } from './TaskDict';
export type SchedulerDict = {
RESCAN: TaskDict;
SWITCH_TITLEDB: TaskDict;
LAUNCHBOX_METADATA: TaskDict;
};

View File

@@ -1,11 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TaskDict = {
ENABLED: boolean;
TITLE: string;
MESSAGE: string;
CRON: string;
};

View File

@@ -0,0 +1,13 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type TaskInfoDict = {
name: string;
manual_run: boolean;
title: string;
description: string;
enabled: boolean;
cron_string: string;
};

View File

@@ -1,10 +0,0 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type WatcherDict = {
ENABLED: boolean;
TITLE: string;
MESSAGE: string;
};

View File

@@ -1,27 +1,97 @@
<script setup lang="ts">
withDefaults(
import { inject, computed } from "vue";
import type { Events } from "@/types/emitter";
import api from "@/services/api/index";
import type { Emitter } from "mitt";
import storeRunningTasks from "@/stores/runningTasks";
// Props
const props = withDefaults(
defineProps<{
enabled: boolean;
enabled?: boolean;
title?: string;
description?: string;
icon?: string;
name?: string;
manual_run?: boolean;
cron_string?: string;
}>(),
{ title: "", description: "", icon: "" },
{
enabled: true,
title: "",
description: "",
icon: "",
name: "",
manual_run: false,
cron_string: "",
},
);
const emitter = inject<Emitter<Events>>("emitter");
const runningTasksStore = storeRunningTasks();
// Computed properties
const isTaskRunning = computed(() =>
props.name ? runningTasksStore.isTaskRunning(props.name) : false,
);
// Functions
function run() {
if (!props.name) return;
// Add task to running tasks
runningTasksStore.addTask(props.name);
api
.post(`/tasks/run/${props.name}`)
.then(() => {
emitter?.emit("snackbarShow", {
msg: `Task '${props.title}' ran successfully!`,
icon: "mdi-check-bold",
color: "green",
});
})
.catch((error) => {
console.log(error);
emitter?.emit("snackbarShow", {
msg: error.response.data.detail,
icon: "mdi-close-circle",
color: "red",
});
})
.finally(() => {
// Remove task from running tasks
runningTasksStore.removeTask(props.name);
});
}
</script>
<template>
<v-card elevation="0" :disabled="!enabled" class="bg-background">
<v-list-item class="pa-0">
<v-list-item-title
class="font-weight-bold"
:class="{ 'text-primary': enabled }"
>{{ title }}</v-list-item-title
>
<v-list-item-subtitle>{{ description }}</v-list-item-subtitle>
<template #prepend
><v-icon
:class="enabled ? 'text-primary' : ''"
:icon="icon" /></template
></v-list-item>
<v-card elevation="0" class="bg-background">
<v-row no-gutters>
<v-col>
<v-list-item :disabled="!enabled" class="pa-0">
<template #prepend>
<v-icon :class="enabled ? 'text-primary' : ''" :icon="icon" />
</template>
<v-list-item-title
class="font-weight-bold"
:class="{ 'text-primary': enabled }"
>{{ title }}</v-list-item-title
>
<v-list-item-subtitle>{{ description }}</v-list-item-subtitle>
</v-list-item>
</v-col>
<v-col v-if="manual_run" cols="auto" class="d-flex align-center">
<v-btn
variant="outlined"
size="small"
class="text-primary"
:disabled="isTaskRunning"
:loading="isTaskRunning"
@click="run"
>
<v-icon>mdi-play</v-icon>
</v-btn>
</v-col>
</v-row>
</v-card>
</template>

View File

@@ -2,102 +2,89 @@
import Task from "@/components/Settings/Administration/TaskOption.vue";
import RSection from "@/components/common/RSection.vue";
import api from "@/services/api/index";
import storeHeartbeat from "@/stores/heartbeat";
import storeRunningTasks from "@/stores/runningTasks";
import type { Events } from "@/types/emitter";
import type { TaskInfoDict } from "@/__generated__/models/TaskInfoDict";
import { convertCronExperssion } from "@/utils";
import type { Emitter } from "mitt";
import { computed, inject } from "vue";
import { computed, onMounted, ref } from "vue";
// Props
const emitter = inject<Emitter<Events>>("emitter");
const heartbeatStore = storeHeartbeat();
const runningTasks = storeRunningTasks();
const tasks = computed(() => [
{
title: heartbeatStore.value.WATCHER.TITLE,
description: heartbeatStore.value.WATCHER.MESSAGE,
icon: heartbeatStore.value.WATCHER.ENABLED
? "mdi-file-check-outline"
: "mdi-file-remove-outline",
enabled: heartbeatStore.value.WATCHER.ENABLED,
},
{
title: heartbeatStore.value.SCHEDULER.RESCAN.TITLE,
description:
heartbeatStore.value.SCHEDULER.RESCAN.MESSAGE +
" " +
convertCronExperssion(heartbeatStore.value.SCHEDULER.RESCAN.CRON),
icon: heartbeatStore.value.SCHEDULER.RESCAN.ENABLED
? "mdi-clock-check-outline"
: "mdi-clock-remove-outline",
enabled: heartbeatStore.value.SCHEDULER.RESCAN.ENABLED,
},
{
title: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.TITLE,
description:
heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.MESSAGE +
" " +
convertCronExperssion(heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.CRON),
icon: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.ENABLED
? "mdi-clock-check-outline"
: "mdi-clock-remove-outline",
enabled: heartbeatStore.value.SCHEDULER.SWITCH_TITLEDB.ENABLED,
},
{
title: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.TITLE,
description:
heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.MESSAGE +
" " +
convertCronExperssion(
heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.CRON,
),
icon: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.ENABLED
? "mdi-clock-check-outline"
: "mdi-clock-remove-outline",
enabled: heartbeatStore.value.SCHEDULER.LAUNCHBOX_METADATA.ENABLED,
},
]);
const tasks = ref<{
watcher?: Array<TaskInfoDict>;
scheduled?: Array<TaskInfoDict>;
manual?: Array<TaskInfoDict>;
}>({});
const watcherTasks = computed(
() =>
tasks.value.watcher?.map((task) => ({
title: task.title,
description: task.description,
icon: task.enabled ? "mdi-file-check-outline" : "mdi-file-remove-outline",
enabled: task.enabled,
name: task.name,
})) || [],
);
const scheduledTasks = computed(
() =>
tasks.value.scheduled?.map((task) => ({
title: task.title,
description:
task.description + " " + convertCronExperssion(task.cron_string),
icon: task.enabled
? "mdi-clock-check-outline"
: "mdi-clock-remove-outline",
enabled: task.enabled,
name: task.name,
manual_run: task.manual_run,
cron_string: convertCronExperssion(task.cron_string),
})) || [],
);
// Icon mapping for manual tasks
const getManualTaskIcon = (taskName: string) => {
const iconMap: Record<string, string> = {
cleanup_orphaned_resources: "mdi-broom",
};
return iconMap[taskName] || "mdi-play";
};
const manualTasks = computed(
() =>
tasks.value.manual?.map((task) => ({
title: task.title,
description: task.description,
icon: getManualTaskIcon(task.name),
name: task.name,
manual_run: task.manual_run,
})) || [],
);
// Functions
const runAllTasks = async () => {
runningTasks.value = true;
const result = await api.post("/tasks/run");
runningTasks.value = false;
if (result.status !== 200) {
return emitter?.emit("snackbarShow", {
msg: "Error running tasks",
icon: "mdi-close-circle",
color: "red",
});
}
emitter?.emit("snackbarShow", {
msg: result.data.msg,
icon: "mdi-check-circle",
color: "green",
const getAvailableTasks = async () => {
await api.get("/tasks").then((response) => {
tasks.value = response.data;
});
console.log(tasks.value);
};
onMounted(() => {
getAvailableTasks();
});
</script>
<template>
<r-section icon="mdi-pulse" title="Tasks" class="ma-2">
<template #toolbar-append>
<v-btn-group class="mr-3" divided density="compact">
<v-btn
:disabled="runningTasks.value"
:loading="runningTasks.value"
prepend-icon="mdi-play"
variant="outlined"
class="text-primary"
@click="runAllTasks"
>
Run All
</v-btn>
</v-btn-group>
</template>
<template #toolbar-append> </template>
<template #content>
<v-row no-gutters class="align-center">
<v-col cols="12" md="6" v-for="task in tasks">
<v-chip
label
variant="text"
prepend-icon="mdi-folder-eye"
class="ml-2 mt-1"
>Watcher</v-chip
>
<v-divider class="border-opacity-25 ma-1" />
<v-row no-gutters class="align-center py-1">
<v-col cols="12" md="6" v-for="task in watcherTasks">
<task
class="ma-3"
:enabled="task.enabled"
@@ -107,6 +94,45 @@ const runAllTasks = async () => {
/>
</v-col>
</v-row>
<v-chip label variant="text" prepend-icon="mdi-clock" class="ml-2 mt-1"
>Scheduled</v-chip
>
<v-divider class="border-opacity-25 ma-1" />
<v-row no-gutters class="align-center py-1">
<v-col cols="12" md="6" v-for="task in scheduledTasks">
<task
class="ma-3"
:enabled="task.enabled"
:title="task.title"
:description="task.description"
:icon="task.icon"
:name="task.name"
:manual_run="task.manual_run"
:cron_string="task.cron_string"
/>
</v-col>
</v-row>
<v-row no-gutters class="align-center py-1">
<v-chip
label
variant="text"
prepend-icon="mdi-gesture-double-tap"
class="ml-2 mt-1"
>Manual</v-chip
>
<v-divider class="border-opacity-25 ma-1" />
<v-col cols="12" md="6" v-for="task in manualTasks">
<task
class="ma-3"
:title="task.title"
:description="task.description"
:icon="task.icon"
:name="task.name"
:manual_run="task.manual_run"
/>
</v-col>
</v-row>
</template>
</r-section>
</template>

View File

@@ -168,7 +168,7 @@ onMounted(() => {
/>
</template>
<template #item.actions="{ item }">
<v-btn-group divided density="compact">
<v-btn-group divided density="compact" variant="text">
<v-btn
size="small"
@click="emitter?.emit('showEditUserDialog', item)"

View File

@@ -15,31 +15,6 @@ const defaultHeartbeat: Heartbeat = {
VERSION: "0.0.0",
SHOW_SETUP_WIZARD: false,
},
WATCHER: {
ENABLED: false,
TITLE: "",
MESSAGE: "",
},
SCHEDULER: {
RESCAN: {
ENABLED: false,
TITLE: "",
MESSAGE: "",
CRON: "",
},
SWITCH_TITLEDB: {
ENABLED: false,
TITLE: "",
MESSAGE: "",
CRON: "",
},
LAUNCHBOX_METADATA: {
ENABLED: false,
TITLE: "",
MESSAGE: "",
CRON: "",
},
},
METADATA_SOURCES: {
ANY_SOURCE_ENABLED: false,
IGDB_API_ENABLED: false,

View File

@@ -1,14 +1,27 @@
import { defineStore } from "pinia";
export default defineStore("runningTasks", {
state: () => ({ value: false }),
state: () => ({
runningTasks: [] as string[],
}),
actions: {
set(runningTasks: boolean) {
this.value = runningTasks;
},
reset() {
this.value = false;
this.runningTasks = [];
},
addTask(taskEndpoint: string) {
if (!this.runningTasks.includes(taskEndpoint)) {
this.runningTasks.push(taskEndpoint);
}
},
removeTask(taskEndpoint: string) {
const index = this.runningTasks.indexOf(taskEndpoint);
if (index > -1) {
this.runningTasks.splice(index, 1);
}
},
isTaskRunning(taskEndpoint: string): boolean {
return this.runningTasks.includes(taskEndpoint);
},
},
});