mirror of
https://github.com/rommapp/romm.git
synced 2026-06-28 06:46:00 +00:00
support states for more types of tasks
This commit is contained in:
@@ -140,6 +140,9 @@ SCAN_TIMEOUT: Final = int(os.environ.get("SCAN_TIMEOUT", 60 * 60 * 4)) # 4 hour
|
||||
|
||||
# TASKS
|
||||
TASK_TIMEOUT: Final = int(os.environ.get("TASK_TIMEOUT", 60 * 5)) # 5 minutes
|
||||
TASK_RESULT_TTL: Final = int(
|
||||
os.environ.get("TASK_RESULT_TTL", 24 * 60 * 60)
|
||||
) # 24 hours
|
||||
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE: Final = str_to_bool(
|
||||
os.environ.get("ENABLE_RESCAN_ON_FILESYSTEM_CHANGE", "false")
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ class TaskExecutionResponse(TypedDict):
|
||||
class TaskStatusResponse(TaskExecutionResponse):
|
||||
started_at: str | None
|
||||
ended_at: str | None
|
||||
result: str | None
|
||||
result: dict[str, Any] | None
|
||||
meta: dict[str, Any] | None
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import socketio # type: ignore
|
||||
from rq import Worker, get_current_job
|
||||
from rq.job import Job
|
||||
|
||||
from config import REDIS_URL, SCAN_TIMEOUT
|
||||
from config import REDIS_URL, SCAN_TIMEOUT, TASK_RESULT_TTL
|
||||
from endpoints.responses.platform import PlatformSchema
|
||||
from endpoints.responses.rom import SimpleRomSchema
|
||||
from exceptions.fs_exceptions import (
|
||||
@@ -629,6 +629,7 @@ async def scan_handler(_sid: str, options: dict[str, Any]):
|
||||
roms_ids,
|
||||
metadata_sources,
|
||||
job_timeout=SCAN_TIMEOUT, # Timeout (default of 4 hours)
|
||||
result_ttl=TASK_RESULT_TTL,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from rq.registry import FailedJobRegistry, FinishedJobRegistry
|
||||
from config import (
|
||||
ENABLE_RESCAN_ON_FILESYSTEM_CHANGE,
|
||||
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
|
||||
TASK_RESULT_TTL,
|
||||
TASK_TIMEOUT,
|
||||
)
|
||||
from decorators.auth import protected_route
|
||||
@@ -305,7 +306,12 @@ async def run_all_tasks(request: Request) -> list[TaskExecutionResponse]:
|
||||
)
|
||||
|
||||
jobs = [
|
||||
(task_name, low_prio_queue.enqueue(task_instance.run, job_timeout=TASK_TIMEOUT))
|
||||
(
|
||||
task_name,
|
||||
low_prio_queue.enqueue(
|
||||
task_instance.run, job_timeout=TASK_TIMEOUT, result_ttl=TASK_RESULT_TTL
|
||||
),
|
||||
)
|
||||
for task_name, task_instance in runnable_tasks.items()
|
||||
]
|
||||
|
||||
@@ -346,7 +352,9 @@ 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_timeout=TASK_TIMEOUT)
|
||||
job = low_prio_queue.enqueue(
|
||||
task_instance.run, job_timeout=TASK_TIMEOUT, result_ttl=TASK_RESULT_TTL
|
||||
)
|
||||
|
||||
return {
|
||||
"task_name": task_name,
|
||||
|
||||
@@ -19,7 +19,7 @@ class CleanupOrphanedResourcesTask(Task):
|
||||
)
|
||||
|
||||
@initialize_context()
|
||||
async def run(self) -> None:
|
||||
async def run(self) -> dict[str, int]:
|
||||
"""Clean up orphaned resources."""
|
||||
log.info(f"Starting {self.title} task...")
|
||||
|
||||
@@ -28,7 +28,7 @@ class CleanupOrphanedResourcesTask(Task):
|
||||
roms_resources_path = os.path.join(RESOURCES_BASE_PATH, "roms")
|
||||
if not os.path.exists(roms_resources_path):
|
||||
log.info("Resources path does not exist, skipping cleanup")
|
||||
return None
|
||||
return {"removed_count": 0}
|
||||
|
||||
existing_platforms = {
|
||||
str(platform.id) for platform in db_platform_handler.get_platforms()
|
||||
@@ -82,5 +82,7 @@ class CleanupOrphanedResourcesTask(Task):
|
||||
log.info(f"Removed {removed_count} orphaned resource directories")
|
||||
log.info("Cleanup of orphaned resources completed!")
|
||||
|
||||
return {"removed_count": removed_count}
|
||||
|
||||
|
||||
cleanup_orphaned_resources_task = CleanupOrphanedResourcesTask()
|
||||
|
||||
@@ -31,11 +31,11 @@ class ScanLibraryTask(PeriodicTask):
|
||||
func="tasks.scheduled.scan_library.scan_library_task.run",
|
||||
)
|
||||
|
||||
async def run(self):
|
||||
async def run(self) -> dict[str, str]:
|
||||
if not ENABLE_SCHEDULED_RESCAN:
|
||||
log.info("Scheduled library scan not enabled, unscheduling...")
|
||||
self.unschedule()
|
||||
return None
|
||||
return {"status": "skipped", "reason": "Scheduled library scan not enabled"}
|
||||
|
||||
source_mapping: dict[str, bool] = {
|
||||
MetadataSource.IGDB: meta_igdb_handler.is_enabled(),
|
||||
@@ -54,7 +54,7 @@ class ScanLibraryTask(PeriodicTask):
|
||||
if not metadata_sources:
|
||||
log.warning("No metadata sources enabled, unscheduling library scan")
|
||||
self.unschedule()
|
||||
return None
|
||||
return {"status": "skipped", "reason": "No metadata sources enabled"}
|
||||
|
||||
log.info("Scheduled library scan started...")
|
||||
await scan_platforms(
|
||||
@@ -62,5 +62,7 @@ class ScanLibraryTask(PeriodicTask):
|
||||
)
|
||||
log.info("Scheduled library scan done")
|
||||
|
||||
return {"status": "completed", "message": "Library scan completed successfully"}
|
||||
|
||||
|
||||
scan_library_task = ScanLibraryTask()
|
||||
|
||||
@@ -24,10 +24,14 @@ class SyncRetroAchievementsProgressTask(PeriodicTask):
|
||||
)
|
||||
|
||||
@initialize_context()
|
||||
async def run(self) -> None:
|
||||
async def run(self) -> dict[str, str | int]:
|
||||
if not meta_ra_handler.is_enabled():
|
||||
log.warning("RetroAchievements API is not enabled, skipping progress sync")
|
||||
return None
|
||||
return {
|
||||
"status": "skipped",
|
||||
"reason": "RetroAchievements API not enabled",
|
||||
"updated_users": 0,
|
||||
}
|
||||
|
||||
log.info("Scheduled RetroAchievements progress sync started...")
|
||||
|
||||
@@ -57,5 +61,7 @@ class SyncRetroAchievementsProgressTask(PeriodicTask):
|
||||
f"Scheduled RetroAchievements progress sync done. Updated users: {len(users)}"
|
||||
)
|
||||
|
||||
return {"status": "completed", "updated_users": len(users)}
|
||||
|
||||
|
||||
sync_retroachievements_progress_task = SyncRetroAchievementsProgressTask()
|
||||
|
||||
@@ -38,15 +38,15 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
|
||||
)
|
||||
|
||||
@initialize_context()
|
||||
async def run(self, force: bool = False) -> None:
|
||||
async def run(self, force: bool = False) -> dict[str, Any]:
|
||||
if not meta_launchbox_handler.is_enabled():
|
||||
log.warning("Launchbox API is not enabled, skipping metadata update")
|
||||
return None
|
||||
return {"status": "skipped", "reason": "Launchbox API not enabled"}
|
||||
|
||||
content = await super().run(force)
|
||||
if content is None:
|
||||
log.warning("No content received from launchbox metadata update")
|
||||
return None
|
||||
return {"status": "failed", "reason": "No content received"}
|
||||
|
||||
try:
|
||||
zip_file_bytes = BytesIO(content)
|
||||
@@ -239,9 +239,13 @@ class UpdateLaunchboxMetadataTask(RemoteFilePullTask):
|
||||
|
||||
except zipfile.BadZipFile:
|
||||
log.error("Bad zip file in launchbox metadata update")
|
||||
return None
|
||||
return {"status": "failed", "reason": "Bad zip file"}
|
||||
|
||||
log.info("Scheduled launchbox metadata update completed!")
|
||||
return {
|
||||
"status": "completed",
|
||||
"message": "Launchbox metadata update completed successfully",
|
||||
}
|
||||
|
||||
|
||||
update_launchbox_metadata_task = UpdateLaunchboxMetadataTask()
|
||||
|
||||
@@ -28,10 +28,10 @@ class UpdateSwitchTitleDBTask(RemoteFilePullTask):
|
||||
)
|
||||
|
||||
@initialize_context()
|
||||
async def run(self, force: bool = False) -> None:
|
||||
async def run(self, force: bool = False) -> dict[str, str]:
|
||||
content = await super().run(force)
|
||||
if content is None:
|
||||
return None
|
||||
return {"status": "failed", "reason": "No content received"}
|
||||
|
||||
index_json = json.loads(content)
|
||||
relevant_data = {k: v for k, v in index_json.items() if k and v}
|
||||
@@ -52,5 +52,10 @@ class UpdateSwitchTitleDBTask(RemoteFilePullTask):
|
||||
|
||||
log.info("Scheduled switch titledb update completed!")
|
||||
|
||||
return {
|
||||
"status": "completed",
|
||||
"message": "Switch TitleDB update completed successfully",
|
||||
}
|
||||
|
||||
|
||||
update_switch_titledb_task = UpdateSwitchTitleDBTask()
|
||||
|
||||
@@ -115,7 +115,7 @@ class RemoteFilePullTask(PeriodicTask, ABC):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
||||
async def run(self, force: bool = False) -> bytes | None:
|
||||
async def run(self, force: bool = False) -> Any:
|
||||
if not self.enabled and not force:
|
||||
log.info(f"Scheduled {self.description} not enabled, unscheduling...")
|
||||
self.unschedule()
|
||||
|
||||
@@ -15,6 +15,7 @@ from config import (
|
||||
RESCAN_ON_FILESYSTEM_CHANGE_DELAY,
|
||||
SCAN_TIMEOUT,
|
||||
SENTRY_DSN,
|
||||
TASK_RESULT_TTL,
|
||||
)
|
||||
from config.config_manager import config_manager as cm
|
||||
from endpoints.sockets.scan import scan_platforms
|
||||
@@ -144,6 +145,7 @@ def process_changes(changes: Sequence[Change]) -> None:
|
||||
scan_type=ScanType.UNIDENTIFIED,
|
||||
metadata_sources=metadata_sources,
|
||||
timeout=SCAN_TIMEOUT,
|
||||
result_ttl=TASK_RESULT_TTL,
|
||||
)
|
||||
return
|
||||
|
||||
@@ -167,6 +169,7 @@ def process_changes(changes: Sequence[Change]) -> None:
|
||||
scan_type=ScanType.QUICK,
|
||||
metadata_sources=metadata_sources,
|
||||
timeout=SCAN_TIMEOUT,
|
||||
result_ttl=TASK_RESULT_TTL,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ RESCAN_ON_FILESYSTEM_CHANGE_DELAY=5
|
||||
|
||||
# Tasks (optional)
|
||||
TASK_TIMEOUT=300
|
||||
TASK_RESULT_TTL=86400
|
||||
ENABLE_SCHEDULED_RESCAN=true
|
||||
SCHEDULED_RESCAN_CRON=0 3 * * *
|
||||
ENABLE_SCHEDULED_UPDATE_SWITCH_TITLEDB=true
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from "vue";
|
||||
import type { TaskStatusResponse } from "@/__generated__";
|
||||
import TaskProgressDisplay from "./tasks/TaskProgressDisplay.vue";
|
||||
import type {
|
||||
ScanStats,
|
||||
ConversionStats,
|
||||
CleanupStats,
|
||||
DownloadProgress,
|
||||
TaskType,
|
||||
ProgressPercentages,
|
||||
} from "./tasks/task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskStatusResponse;
|
||||
@@ -34,63 +43,139 @@ const formatDateTime = (dateTime: string | null) => {
|
||||
};
|
||||
|
||||
// Extract scan stats from meta if available
|
||||
const scanStats = computed(() => {
|
||||
const scanStats = computed((): ScanStats | null => {
|
||||
if (props.task.meta?.scan_stats) {
|
||||
return props.task.meta.scan_stats;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Format scan progress for display
|
||||
const scanProgress = computed(() => {
|
||||
if (!scanStats.value) return null;
|
||||
|
||||
const stats = scanStats.value;
|
||||
const totalPlatforms = stats.total_platforms || 0;
|
||||
const totalRoms = stats.total_roms || 0;
|
||||
const scannedPlatforms = stats.scanned_platforms || 0;
|
||||
const scannedRoms = stats.scanned_roms || 0;
|
||||
|
||||
return {
|
||||
platforms: `${scannedPlatforms}/${totalPlatforms}`,
|
||||
roms: `${scannedRoms}/${totalRoms}`,
|
||||
addedRoms: stats.added_roms || 0,
|
||||
metadataRoms: stats.metadata_roms || 0,
|
||||
scannedFirmware: stats.scanned_firmware || 0,
|
||||
addedFirmware: stats.added_firmware || 0,
|
||||
};
|
||||
// Extract conversion stats from meta if available
|
||||
const conversionStats = computed((): ConversionStats | null => {
|
||||
if (props.task.meta?.processed_count !== undefined) {
|
||||
return {
|
||||
processed: props.task.meta.processed_count || 0,
|
||||
errors: props.task.meta.error_count || 0,
|
||||
total: props.task.meta.total_files || 0,
|
||||
errorList: props.task.meta.errors || [],
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Check if this is a scan task
|
||||
const isScanTask = computed(() => {
|
||||
return (
|
||||
props.task.task_name?.toLowerCase().includes("scan") ||
|
||||
props.task.meta?.scan_stats
|
||||
);
|
||||
// Extract cleanup stats from result if available
|
||||
const cleanupStats = computed((): CleanupStats | null => {
|
||||
if (props.task.result && typeof props.task.result === "object") {
|
||||
const result = props.task.result as any;
|
||||
if (result.removed_count !== undefined) {
|
||||
return {
|
||||
removed: result.removed_count || 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Extract download progress from meta if available
|
||||
const downloadProgress = computed((): DownloadProgress | null => {
|
||||
if (props.task.meta?.download_progress !== undefined) {
|
||||
return {
|
||||
progress: props.task.meta.download_progress || 0,
|
||||
total: props.task.meta.download_total || 0,
|
||||
current: props.task.meta.download_current || 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
// Check task type
|
||||
const taskType = computed((): TaskType => {
|
||||
const taskName = props.task.task_name?.toLowerCase() || "";
|
||||
|
||||
if (taskName.includes("scan") || props.task.meta?.scan_stats) {
|
||||
return "scan";
|
||||
}
|
||||
if (
|
||||
taskName.includes("convert") ||
|
||||
taskName.includes("webp") ||
|
||||
conversionStats.value
|
||||
) {
|
||||
return "conversion";
|
||||
}
|
||||
if (
|
||||
taskName.includes("cleanup") ||
|
||||
taskName.includes("orphan") ||
|
||||
cleanupStats.value
|
||||
) {
|
||||
return "cleanup";
|
||||
}
|
||||
if (
|
||||
taskName.includes("update") ||
|
||||
taskName.includes("metadata") ||
|
||||
taskName.includes("launchbox") ||
|
||||
taskName.includes("switch") ||
|
||||
downloadProgress.value
|
||||
) {
|
||||
return "update";
|
||||
}
|
||||
if (taskName.includes("watcher") || taskName.includes("filesystem")) {
|
||||
return "watcher";
|
||||
}
|
||||
|
||||
return "generic";
|
||||
});
|
||||
|
||||
// Expandable details state
|
||||
const showDetails = ref(false);
|
||||
|
||||
// Calculate progress percentages
|
||||
const progressPercentages = computed(() => {
|
||||
if (!scanStats.value) return null;
|
||||
const progressPercentages = computed((): ProgressPercentages | null => {
|
||||
if (taskType.value === "scan" && scanStats.value) {
|
||||
const stats = scanStats.value;
|
||||
const platformProgress =
|
||||
stats.total_platforms > 0
|
||||
? Math.round((stats.scanned_platforms / stats.total_platforms) * 100)
|
||||
: 0;
|
||||
const romProgress =
|
||||
stats.total_roms > 0
|
||||
? Math.round((stats.scanned_roms / stats.total_roms) * 100)
|
||||
: 0;
|
||||
|
||||
const stats = scanStats.value;
|
||||
const platformProgress =
|
||||
stats.total_platforms > 0
|
||||
? Math.round((stats.scanned_platforms / stats.total_platforms) * 100)
|
||||
: 0;
|
||||
const romProgress =
|
||||
stats.total_roms > 0
|
||||
? Math.round((stats.scanned_roms / stats.total_roms) * 100)
|
||||
: 0;
|
||||
return {
|
||||
platforms: platformProgress,
|
||||
roms: romProgress,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
platforms: platformProgress,
|
||||
roms: romProgress,
|
||||
};
|
||||
if (taskType.value === "conversion" && conversionStats.value) {
|
||||
const stats = conversionStats.value;
|
||||
const total = stats.total || 0;
|
||||
const processed = stats.processed || 0;
|
||||
const progress = total > 0 ? Math.round((processed / total) * 100) : 0;
|
||||
|
||||
return {
|
||||
conversion: progress,
|
||||
};
|
||||
}
|
||||
|
||||
if (taskType.value === "update" && downloadProgress.value) {
|
||||
const progress = downloadProgress.value;
|
||||
const downloadProgressPercent =
|
||||
progress.total > 0
|
||||
? Math.round((progress.current / progress.total) * 100)
|
||||
: 0;
|
||||
|
||||
return {
|
||||
download: downloadProgressPercent,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
|
||||
const toggleDetails = () => {
|
||||
showDetails.value = !showDetails.value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -114,149 +199,22 @@ const progressPercentages = computed(() => {
|
||||
Started: {{ formatDateTime(task.started_at) }}
|
||||
</span>
|
||||
|
||||
<!-- Scan Progress Display -->
|
||||
<div v-if="isScanTask && scanProgress" class="mt-2">
|
||||
<v-divider class="mb-2" />
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<div class="text-caption text-blue-grey-lighten-1">
|
||||
Scan Progress
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="scanStats"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:icon="showDetails ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
@click="showDetails = !showDetails"
|
||||
/>
|
||||
</div>
|
||||
<!-- Progress Bars -->
|
||||
<div v-if="progressPercentages" class="mb-3">
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">Platforms</span>
|
||||
<span class="text-caption"
|
||||
>{{ progressPercentages.platforms }}%</span
|
||||
>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.platforms"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">ROMs</span>
|
||||
<span class="text-caption"
|
||||
>{{ progressPercentages.roms }}%</span
|
||||
>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.roms"
|
||||
color="secondary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Chips -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="primary" variant="outlined">
|
||||
Platforms: {{ scanProgress.platforms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="secondary" variant="outlined">
|
||||
ROMs: {{ scanProgress.roms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="success" variant="outlined">
|
||||
Added: {{ scanProgress.addedRoms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="info" variant="outlined">
|
||||
Metadata: {{ scanProgress.metadataRoms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="warning" variant="outlined">
|
||||
Firmware: {{ scanProgress.scannedFirmware }}
|
||||
</v-chip>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Scan Stats -->
|
||||
<v-expand-transition>
|
||||
<div v-if="showDetails && scanStats" class="mt-3">
|
||||
<v-card variant="outlined" class="pa-3">
|
||||
<div class="text-caption text-blue-grey-lighten-1 mb-2">
|
||||
Detailed Statistics
|
||||
</div>
|
||||
<v-row dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Total Platforms</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.total_platforms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned Platforms</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.scanned_platforms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">New Platforms</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.new_platforms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Identified Platforms</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.identified_platforms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Total ROMs</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.total_roms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned ROMs</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.scanned_roms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Added ROMs</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.added_roms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Metadata ROMs</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.metadata_roms || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned Firmware</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.scanned_firmware || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Added Firmware</div>
|
||||
<div class="text-h6">
|
||||
{{ scanStats.added_firmware || 0 }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
<!-- Task Progress Display -->
|
||||
<TaskProgressDisplay
|
||||
:task="task"
|
||||
:task-type="taskType"
|
||||
:scan-stats="scanStats"
|
||||
:conversion-stats="conversionStats"
|
||||
:cleanup-stats="cleanupStats"
|
||||
:download-progress="downloadProgress"
|
||||
:progress-percentages="progressPercentages"
|
||||
:show-details="showDetails"
|
||||
@toggle-details="toggleDetails"
|
||||
/>
|
||||
|
||||
<!-- Generic Meta Data Display -->
|
||||
<div
|
||||
v-else-if="task.meta && Object.keys(task.meta).length > 0"
|
||||
v-if="task.meta && Object.keys(task.meta).length > 0"
|
||||
class="mt-2"
|
||||
>
|
||||
<v-divider class="mb-2" />
|
||||
|
||||
@@ -10,7 +10,6 @@ import { convertCronExperssion } from "@/utils";
|
||||
const tasksStore = storeTasks();
|
||||
const { watcherTasks, scheduledTasks, manualTasks, taskStatuses } =
|
||||
storeToRefs(tasksStore);
|
||||
const isLoadingRunningTasks = ref(false);
|
||||
|
||||
const watcherTasksUI = computed(() =>
|
||||
watcherTasks.value.map((task) => ({
|
||||
@@ -103,13 +102,10 @@ const getManualTaskIcon = (taskName: string) => {
|
||||
|
||||
// Fetch task status
|
||||
const fetchTaskStatus = async () => {
|
||||
isLoadingRunningTasks.value = true;
|
||||
try {
|
||||
await tasksStore.fetchTaskStatus();
|
||||
} catch (error) {
|
||||
console.error("Error fetching task status:", error);
|
||||
} finally {
|
||||
isLoadingRunningTasks.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -199,31 +195,18 @@ onUnmounted(() => {
|
||||
</v-card>
|
||||
</div>
|
||||
<v-row no-gutters class="align-center py-1">
|
||||
<v-col
|
||||
v-if="taskStatuses.length === 0 && !isLoadingRunningTasks"
|
||||
cols="12"
|
||||
>
|
||||
<v-col v-if="taskStatuses.length === 0" cols="12">
|
||||
<v-card elevation="0" class="bg-background ma-3">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon color="grey">mdi-information-outline</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-grey">
|
||||
No tasks currently running
|
||||
No currently running tasks
|
||||
</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col v-else-if="isLoadingRunningTasks" cols="12">
|
||||
<v-card elevation="0" class="bg-background ma-3">
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-progress-circular indeterminate size="24" />
|
||||
</template>
|
||||
<v-list-item-title> Loading running tasks... </v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-else
|
||||
v-for="task in taskStatuses"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import type { CleanupStats } from "./task-types";
|
||||
|
||||
defineProps<{
|
||||
cleanupStats: CleanupStats;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="success" variant="outlined">
|
||||
Removed: {{ cleanupStats.removed }} items
|
||||
</v-chip>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,58 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type {
|
||||
ConversionStats,
|
||||
TaskProgress,
|
||||
ProgressPercentages,
|
||||
} from "./task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
conversionStats: ConversionStats;
|
||||
progressPercentages: ProgressPercentages | null;
|
||||
}>();
|
||||
|
||||
const conversionProgress = computed((): TaskProgress => {
|
||||
const stats = props.conversionStats;
|
||||
const total = stats.total || 0;
|
||||
const processed = stats.processed || 0;
|
||||
const errors = stats.errors || 0;
|
||||
|
||||
return {
|
||||
processed: `${processed}/${total}`,
|
||||
errors: errors,
|
||||
successRate:
|
||||
total > 0 ? Math.round(((processed - errors) / total) * 100) : 0,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Progress Bar -->
|
||||
<div v-if="progressPercentages" class="mb-3">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">Conversion Progress</span>
|
||||
<span class="text-caption">{{ progressPercentages.conversion }}%</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.conversion"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Summary Chips -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="primary" variant="outlined">
|
||||
Processed: {{ conversionProgress.processed }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="error" variant="outlined">
|
||||
Errors: {{ conversionProgress.errors }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="success" variant="outlined">
|
||||
Success Rate: {{ conversionProgress.successRate }}%
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type {
|
||||
ScanStats,
|
||||
TaskProgress,
|
||||
ProgressPercentages,
|
||||
} from "./task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
scanStats: ScanStats;
|
||||
progressPercentages: ProgressPercentages | null;
|
||||
}>();
|
||||
|
||||
const scanProgress = computed((): TaskProgress => {
|
||||
const stats = props.scanStats;
|
||||
const totalPlatforms = stats.total_platforms || 0;
|
||||
const totalRoms = stats.total_roms || 0;
|
||||
const scannedPlatforms = stats.scanned_platforms || 0;
|
||||
const scannedRoms = stats.scanned_roms || 0;
|
||||
|
||||
return {
|
||||
platforms: `${scannedPlatforms}/${totalPlatforms}`,
|
||||
roms: `${scannedRoms}/${totalRoms}`,
|
||||
addedRoms: stats.added_roms || 0,
|
||||
metadataRoms: stats.metadata_roms || 0,
|
||||
scannedFirmware: stats.scanned_firmware || 0,
|
||||
addedFirmware: stats.added_firmware || 0,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Progress Bars -->
|
||||
<div v-if="progressPercentages" class="mb-3">
|
||||
<div class="mb-2">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">Platforms</span>
|
||||
<span class="text-caption">{{ progressPercentages.platforms }}%</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.platforms"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">ROMs</span>
|
||||
<span class="text-caption">{{ progressPercentages.roms }}%</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.roms"
|
||||
color="secondary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary Chips -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="primary" variant="outlined">
|
||||
Platforms: {{ scanProgress.platforms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="secondary" variant="outlined">
|
||||
ROMs: {{ scanProgress.roms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="success" variant="outlined">
|
||||
Added: {{ scanProgress.addedRoms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="info" variant="outlined">
|
||||
Metadata: {{ scanProgress.metadataRoms }}
|
||||
</v-chip>
|
||||
<v-chip size="x-small" color="warning" variant="outlined">
|
||||
Firmware: {{ scanProgress.scannedFirmware }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import type {
|
||||
ScanStats,
|
||||
ConversionStats,
|
||||
CleanupStats,
|
||||
DownloadProgress,
|
||||
TaskType,
|
||||
} from "./task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
taskType: TaskType;
|
||||
scanStats?: ScanStats | null;
|
||||
conversionStats?: ConversionStats | null;
|
||||
cleanupStats?: CleanupStats | null;
|
||||
downloadProgress?: DownloadProgress | null;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card variant="outlined" class="pa-3">
|
||||
<div class="text-caption text-blue-grey-lighten-1 mb-2">
|
||||
Detailed Statistics
|
||||
</div>
|
||||
|
||||
<!-- Scan Stats Details -->
|
||||
<v-row v-if="taskType === 'scan' && scanStats" dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Total Platforms</div>
|
||||
<div class="text-h6">{{ scanStats.total_platforms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned Platforms</div>
|
||||
<div class="text-h6">{{ scanStats.scanned_platforms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">New Platforms</div>
|
||||
<div class="text-h6">{{ scanStats.new_platforms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Identified Platforms</div>
|
||||
<div class="text-h6">{{ scanStats.identified_platforms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Total ROMs</div>
|
||||
<div class="text-h6">{{ scanStats.total_roms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned ROMs</div>
|
||||
<div class="text-h6">{{ scanStats.scanned_roms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Added ROMs</div>
|
||||
<div class="text-h6">{{ scanStats.added_roms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Metadata ROMs</div>
|
||||
<div class="text-h6">{{ scanStats.metadata_roms || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Scanned Firmware</div>
|
||||
<div class="text-h6">{{ scanStats.scanned_firmware || 0 }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Added Firmware</div>
|
||||
<div class="text-h6">{{ scanStats.added_firmware || 0 }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Conversion Stats Details -->
|
||||
<v-row v-else-if="taskType === 'conversion' && conversionStats" dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Total Files</div>
|
||||
<div class="text-h6">{{ conversionStats.total }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Processed</div>
|
||||
<div class="text-h6">{{ conversionStats.processed }}</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Errors</div>
|
||||
<div class="text-h6">{{ conversionStats.errors }}</div>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
v-if="conversionStats.errorList && conversionStats.errorList.length > 0"
|
||||
>
|
||||
<div class="text-caption">Error Details</div>
|
||||
<div class="text-caption text-red">
|
||||
{{ conversionStats.errorList.slice(0, 5).join(", ") }}
|
||||
<span v-if="conversionStats.errorList.length > 5">...</span>
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Cleanup Stats Details -->
|
||||
<v-row v-else-if="taskType === 'cleanup' && cleanupStats" dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Removed Items</div>
|
||||
<div class="text-h6">{{ cleanupStats.removed }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<!-- Download Stats Details -->
|
||||
<v-row v-else-if="taskType === 'update' && downloadProgress" dense>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Downloaded</div>
|
||||
<div class="text-h6">
|
||||
{{ downloadProgress.current }}/{{ downloadProgress.total }}
|
||||
</div>
|
||||
</v-col>
|
||||
<v-col cols="6" sm="4">
|
||||
<div class="text-caption">Progress</div>
|
||||
<div class="text-h6">{{ downloadProgress.progress }}%</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
</template>
|
||||
@@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type { TaskStatusResponse } from "@/__generated__";
|
||||
import CleanupTaskProgress from "./CleanupTaskProgress.vue";
|
||||
import ConversionTaskProgress from "./ConversionTaskProgress.vue";
|
||||
import ScanTaskProgress from "./ScanTaskProgress.vue";
|
||||
import TaskDetailedStats from "./TaskDetailedStats.vue";
|
||||
import UpdateTaskProgress from "./UpdateTaskProgress.vue";
|
||||
import type {
|
||||
ScanStats,
|
||||
ConversionStats,
|
||||
CleanupStats,
|
||||
DownloadProgress,
|
||||
TaskType,
|
||||
ProgressPercentages,
|
||||
} from "./task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
task: TaskStatusResponse;
|
||||
taskType: TaskType;
|
||||
scanStats?: ScanStats | null;
|
||||
conversionStats?: ConversionStats | null;
|
||||
cleanupStats?: CleanupStats | null;
|
||||
downloadProgress?: DownloadProgress | null;
|
||||
progressPercentages: ProgressPercentages | null;
|
||||
showDetails: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
"toggle-details": [];
|
||||
}>();
|
||||
|
||||
const hasDetailedStats = computed(() => {
|
||||
return !!(
|
||||
props.scanStats ||
|
||||
props.conversionStats ||
|
||||
props.cleanupStats ||
|
||||
props.downloadProgress
|
||||
);
|
||||
});
|
||||
|
||||
const progressTitle = computed(() => {
|
||||
switch (props.taskType) {
|
||||
case "scan":
|
||||
return "Scan Progress";
|
||||
case "conversion":
|
||||
return "Conversion Progress";
|
||||
case "cleanup":
|
||||
return "Cleanup Progress";
|
||||
case "update":
|
||||
return "Update Progress";
|
||||
default:
|
||||
return "Task Progress";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="taskType !== 'generic' && hasDetailedStats" class="mt-2">
|
||||
<v-divider class="mb-2" />
|
||||
<div class="d-flex align-center justify-space-between mb-1">
|
||||
<div class="text-caption text-blue-grey-lighten-1">
|
||||
{{ progressTitle }}
|
||||
</div>
|
||||
<v-btn
|
||||
v-if="hasDetailedStats"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:icon="showDetails ? 'mdi-chevron-up' : 'mdi-chevron-down'"
|
||||
@click="emit('toggle-details')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Scan Task Progress -->
|
||||
<ScanTaskProgress
|
||||
v-if="taskType === 'scan' && scanStats"
|
||||
:scan-stats="scanStats"
|
||||
:progress-percentages="progressPercentages"
|
||||
/>
|
||||
|
||||
<!-- Conversion Task Progress -->
|
||||
<ConversionTaskProgress
|
||||
v-else-if="taskType === 'conversion' && conversionStats"
|
||||
:conversion-stats="conversionStats"
|
||||
:progress-percentages="progressPercentages"
|
||||
/>
|
||||
|
||||
<!-- Cleanup Task Progress -->
|
||||
<CleanupTaskProgress
|
||||
v-else-if="taskType === 'cleanup' && cleanupStats"
|
||||
:cleanup-stats="cleanupStats"
|
||||
/>
|
||||
|
||||
<!-- Update Task Progress -->
|
||||
<UpdateTaskProgress
|
||||
v-else-if="taskType === 'update' && downloadProgress"
|
||||
:download-progress="downloadProgress"
|
||||
:progress-percentages="progressPercentages"
|
||||
/>
|
||||
|
||||
<!-- Detailed Stats (Expandable) -->
|
||||
<v-expand-transition>
|
||||
<div v-if="showDetails && hasDetailedStats" class="mt-3">
|
||||
<TaskDetailedStats
|
||||
:task-type="taskType"
|
||||
:scan-stats="scanStats"
|
||||
:conversion-stats="conversionStats"
|
||||
:cleanup-stats="cleanupStats"
|
||||
:download-progress="downloadProgress"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import type {
|
||||
DownloadProgress,
|
||||
TaskProgress,
|
||||
ProgressPercentages,
|
||||
} from "./task-types";
|
||||
|
||||
const props = defineProps<{
|
||||
downloadProgress: DownloadProgress;
|
||||
progressPercentages: ProgressPercentages | null;
|
||||
}>();
|
||||
|
||||
const updateProgress = computed((): TaskProgress => {
|
||||
const progress = props.downloadProgress;
|
||||
return {
|
||||
downloaded: `${progress.current}/${progress.total}`,
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<!-- Progress Bar -->
|
||||
<div v-if="progressPercentages" class="mb-3">
|
||||
<div class="d-flex justify-space-between align-center mb-1">
|
||||
<span class="text-caption">Download Progress</span>
|
||||
<span class="text-caption">{{ progressPercentages.download }}%</span>
|
||||
</div>
|
||||
<v-progress-linear
|
||||
:model-value="progressPercentages.download"
|
||||
color="primary"
|
||||
height="6"
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Summary Chips -->
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<v-chip size="x-small" color="primary" variant="outlined">
|
||||
Downloaded: {{ updateProgress.downloaded }}
|
||||
</v-chip>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,59 @@
|
||||
// Shared types for task components
|
||||
export interface ScanStats {
|
||||
total_platforms: number;
|
||||
total_roms: number;
|
||||
scanned_platforms: number;
|
||||
new_platforms: number;
|
||||
identified_platforms: number;
|
||||
scanned_roms: number;
|
||||
added_roms: number;
|
||||
metadata_roms: number;
|
||||
scanned_firmware: number;
|
||||
added_firmware: number;
|
||||
}
|
||||
|
||||
export interface ConversionStats {
|
||||
processed: number;
|
||||
errors: number;
|
||||
total: number;
|
||||
errorList: string[];
|
||||
}
|
||||
|
||||
export interface CleanupStats {
|
||||
removed: number;
|
||||
}
|
||||
|
||||
export interface DownloadProgress {
|
||||
progress: number;
|
||||
total: number;
|
||||
current: number;
|
||||
}
|
||||
|
||||
export interface TaskProgress {
|
||||
platforms?: string;
|
||||
roms?: string;
|
||||
addedRoms?: number;
|
||||
metadataRoms?: number;
|
||||
scannedFirmware?: number;
|
||||
addedFirmware?: number;
|
||||
processed?: string;
|
||||
errors?: number;
|
||||
successRate?: number;
|
||||
removed?: number;
|
||||
downloaded?: string;
|
||||
}
|
||||
|
||||
export interface ProgressPercentages {
|
||||
platforms?: number;
|
||||
roms?: number;
|
||||
conversion?: number;
|
||||
download?: number;
|
||||
}
|
||||
|
||||
export type TaskType =
|
||||
| "scan"
|
||||
| "conversion"
|
||||
| "cleanup"
|
||||
| "update"
|
||||
| "watcher"
|
||||
| "generic";
|
||||
Reference in New Issue
Block a user