job logs + better status popup
This commit is contained in:
@@ -13,4 +13,4 @@ import type { ClientOptions as ClientOptions2 } from './types.gen';
|
||||
*/
|
||||
export type CreateClientConfig<T extends ClientOptions = ClientOptions2> = (override?: Config<ClientOptions & T>) => Config<Required<ClientOptions> & T>;
|
||||
|
||||
export const client = createClient(createConfig<ClientOptions2>({ baseUrl: 'http://localhost:8000' }));
|
||||
export const client = createClient(createConfig<ClientOptions2>());
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,33 +1,7 @@
|
||||
// This file is auto-generated by @hey-api/openapi-ts
|
||||
|
||||
export type ClientOptions = {
|
||||
baseUrl: 'http://localhost:8000' | (string & {});
|
||||
};
|
||||
|
||||
/**
|
||||
* BackupJobSchema
|
||||
*/
|
||||
export type BackupJobSchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Job Type
|
||||
*/
|
||||
job_type: string;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Started At
|
||||
*/
|
||||
started_at?: string | null;
|
||||
/**
|
||||
* Completed At
|
||||
*/
|
||||
completed_at?: string | null;
|
||||
baseUrl: `${string}://${string}` | (string & {});
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -44,6 +18,20 @@ export type BatchTrackRequest = {
|
||||
untracks?: Array<string>;
|
||||
};
|
||||
|
||||
/**
|
||||
* BrowseResponseSchema
|
||||
*/
|
||||
export type BrowseResponseSchema = {
|
||||
/**
|
||||
* Files
|
||||
*/
|
||||
files: Array<FileItemSchema>;
|
||||
/**
|
||||
* Last Scan Time
|
||||
*/
|
||||
last_scan_time?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* CartFileItemSchema
|
||||
*/
|
||||
@@ -184,6 +172,44 @@ export type DirectoryCartRequest = {
|
||||
path: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* DiscrepancySchema
|
||||
*/
|
||||
export type DiscrepancySchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Path
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* Size
|
||||
*/
|
||||
size: number;
|
||||
/**
|
||||
* Mtime
|
||||
*/
|
||||
mtime: string;
|
||||
/**
|
||||
* Last Seen Timestamp
|
||||
*/
|
||||
last_seen_timestamp?: string | null;
|
||||
/**
|
||||
* Sha256 Hash
|
||||
*/
|
||||
sha256_hash?: string | null;
|
||||
/**
|
||||
* Is Deleted
|
||||
*/
|
||||
is_deleted: boolean;
|
||||
/**
|
||||
* Has Versions
|
||||
*/
|
||||
has_versions?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* FileItemSchema
|
||||
*/
|
||||
@@ -270,11 +296,18 @@ export type ItemMetadataSchema = {
|
||||
* Sha256 Hash
|
||||
*/
|
||||
sha256_hash?: string | null;
|
||||
|
||||
/**
|
||||
* Is Ignored
|
||||
*/
|
||||
is_ignored?: boolean;
|
||||
/**
|
||||
* Is Deleted
|
||||
*/
|
||||
is_deleted?: boolean;
|
||||
/**
|
||||
* Exists On Disk
|
||||
*/
|
||||
exists_on_disk?: boolean | null;
|
||||
/**
|
||||
* Child Count
|
||||
*/
|
||||
@@ -292,45 +325,21 @@ export type ItemMetadataSchema = {
|
||||
};
|
||||
|
||||
/**
|
||||
* JobSchema
|
||||
* JobLogSchema
|
||||
*/
|
||||
export type JobSchema = {
|
||||
export type JobLogSchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Job Type
|
||||
* Message
|
||||
*/
|
||||
job_type: string;
|
||||
message: string;
|
||||
/**
|
||||
* Status
|
||||
* Timestamp
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Progress
|
||||
*/
|
||||
progress: number;
|
||||
/**
|
||||
* Current Task
|
||||
*/
|
||||
current_task?: string | null;
|
||||
/**
|
||||
* Error Message
|
||||
*/
|
||||
error_message?: string | null;
|
||||
/**
|
||||
* Started At
|
||||
*/
|
||||
started_at?: string | null;
|
||||
/**
|
||||
* Completed At
|
||||
*/
|
||||
completed_at?: string | null;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
created_at: string;
|
||||
timestamp: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -555,6 +564,10 @@ export type ScanStatusSchema = {
|
||||
* Files Modified
|
||||
*/
|
||||
files_modified: number;
|
||||
/**
|
||||
* Files Missing
|
||||
*/
|
||||
files_missing: number;
|
||||
/**
|
||||
* Total Files Found
|
||||
*/
|
||||
@@ -677,6 +690,92 @@ export type ValidationError = {
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* JobSchema
|
||||
*/
|
||||
export type AppApiBackupsJobSchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Job Type
|
||||
*/
|
||||
job_type: string;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Started At
|
||||
*/
|
||||
started_at?: string | null;
|
||||
/**
|
||||
* Completed At
|
||||
*/
|
||||
completed_at?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* JobSchema
|
||||
*/
|
||||
export type AppApiSystemJobSchema = {
|
||||
/**
|
||||
* Id
|
||||
*/
|
||||
id: number;
|
||||
/**
|
||||
* Job Type
|
||||
*/
|
||||
job_type: string;
|
||||
/**
|
||||
* Status
|
||||
*/
|
||||
status: string;
|
||||
/**
|
||||
* Progress
|
||||
*/
|
||||
progress: number;
|
||||
/**
|
||||
* Current Task
|
||||
*/
|
||||
current_task?: string | null;
|
||||
/**
|
||||
* Error Message
|
||||
*/
|
||||
error_message?: string | null;
|
||||
/**
|
||||
* Started At
|
||||
*/
|
||||
started_at?: string | null;
|
||||
/**
|
||||
* Completed At
|
||||
*/
|
||||
completed_at?: string | null;
|
||||
/**
|
||||
* Created At
|
||||
*/
|
||||
created_at: string;
|
||||
/**
|
||||
* Latest Log
|
||||
*/
|
||||
latest_log?: string | null;
|
||||
};
|
||||
|
||||
export type ResetTestEnvironmentSystemTestResetPostData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/system/test/reset';
|
||||
};
|
||||
|
||||
export type ResetTestEnvironmentSystemTestResetPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type GetDashboardStatsSystemDashboardStatsGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -724,7 +823,7 @@ export type ListJobsSystemJobsGetResponses = {
|
||||
*
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<JobSchema>;
|
||||
200: Array<AppApiSystemJobSchema>;
|
||||
};
|
||||
|
||||
export type ListJobsSystemJobsGetResponse = ListJobsSystemJobsGetResponses[keyof ListJobsSystemJobsGetResponses];
|
||||
@@ -768,11 +867,43 @@ export type GetJobDetailSystemJobsJobIdGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: JobSchema;
|
||||
200: AppApiSystemJobSchema;
|
||||
};
|
||||
|
||||
export type GetJobDetailSystemJobsJobIdGetResponse = GetJobDetailSystemJobsJobIdGetResponses[keyof GetJobDetailSystemJobsJobIdGetResponses];
|
||||
|
||||
export type GetJobLogsSystemJobsJobIdLogsGetData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* Job Id
|
||||
*/
|
||||
job_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/system/jobs/{job_id}/logs';
|
||||
};
|
||||
|
||||
export type GetJobLogsSystemJobsJobIdLogsGetErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type GetJobLogsSystemJobsJobIdLogsGetError = GetJobLogsSystemJobsJobIdLogsGetErrors[keyof GetJobLogsSystemJobsJobIdLogsGetErrors];
|
||||
|
||||
export type GetJobLogsSystemJobsJobIdLogsGetResponses = {
|
||||
/**
|
||||
* Response Get Job Logs System Jobs Job Id Logs Get
|
||||
*
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<JobLogSchema>;
|
||||
};
|
||||
|
||||
export type GetJobLogsSystemJobsJobIdLogsGetResponse = GetJobLogsSystemJobsJobIdLogsGetResponses[keyof GetJobLogsSystemJobsJobIdLogsGetResponses];
|
||||
|
||||
export type CancelJobSystemJobsJobIdCancelPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
@@ -882,11 +1013,9 @@ export type BrowseSystemPathSystemBrowseGetError = BrowseSystemPathSystemBrowseG
|
||||
|
||||
export type BrowseSystemPathSystemBrowseGetResponses = {
|
||||
/**
|
||||
* Response Browse System Path System Browse Get
|
||||
*
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<FileItemSchema>;
|
||||
200: BrowseResponseSchema;
|
||||
};
|
||||
|
||||
export type BrowseSystemPathSystemBrowseGetResponse = BrowseSystemPathSystemBrowseGetResponses[keyof BrowseSystemPathSystemBrowseGetResponses];
|
||||
@@ -1159,6 +1288,108 @@ export type GetSystemTreeSystemTreeGetResponses = {
|
||||
|
||||
export type GetSystemTreeSystemTreeGetResponse = GetSystemTreeSystemTreeGetResponses[keyof GetSystemTreeSystemTreeGetResponses];
|
||||
|
||||
export type ListDiscrepanciesSystemDiscrepanciesGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/system/discrepancies';
|
||||
};
|
||||
|
||||
export type ListDiscrepanciesSystemDiscrepanciesGetResponses = {
|
||||
/**
|
||||
* Response List Discrepancies System Discrepancies Get
|
||||
*
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<DiscrepancySchema>;
|
||||
};
|
||||
|
||||
export type ListDiscrepanciesSystemDiscrepanciesGetResponse = ListDiscrepanciesSystemDiscrepanciesGetResponses[keyof ListDiscrepanciesSystemDiscrepanciesGetResponses];
|
||||
|
||||
export type ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* File Id
|
||||
*/
|
||||
file_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/system/discrepancies/{file_id}/confirm';
|
||||
};
|
||||
|
||||
export type ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostError = ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostErrors[keyof ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostErrors];
|
||||
|
||||
export type ConfirmFileDeletedSystemDiscrepanciesFileIdConfirmPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type DismissDiscrepancySystemDiscrepanciesFileIdDismissPostData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* File Id
|
||||
*/
|
||||
file_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/system/discrepancies/{file_id}/dismiss';
|
||||
};
|
||||
|
||||
export type DismissDiscrepancySystemDiscrepanciesFileIdDismissPostErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DismissDiscrepancySystemDiscrepanciesFileIdDismissPostError = DismissDiscrepancySystemDiscrepanciesFileIdDismissPostErrors[keyof DismissDiscrepancySystemDiscrepanciesFileIdDismissPostErrors];
|
||||
|
||||
export type DismissDiscrepancySystemDiscrepanciesFileIdDismissPostResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteData = {
|
||||
body?: never;
|
||||
path: {
|
||||
/**
|
||||
* File Id
|
||||
*/
|
||||
file_id: number;
|
||||
};
|
||||
query?: never;
|
||||
url: '/system/discrepancies/{file_id}';
|
||||
};
|
||||
|
||||
export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteErrors = {
|
||||
/**
|
||||
* Validation Error
|
||||
*/
|
||||
422: HttpValidationError;
|
||||
};
|
||||
|
||||
export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteError = DeleteFileRecordSystemDiscrepanciesFileIdDeleteErrors[keyof DeleteFileRecordSystemDiscrepanciesFileIdDeleteErrors];
|
||||
|
||||
export type DeleteFileRecordSystemDiscrepanciesFileIdDeleteResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type ListStorageProvidersInventoryProvidersGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -1362,6 +1593,20 @@ export type GetSystemAnalyticsInventoryInsightsGetResponses = {
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type DetectUnregisteredMediaInventoryDetectGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
query?: never;
|
||||
url: '/inventory/detect';
|
||||
};
|
||||
|
||||
export type DetectUnregisteredMediaInventoryDetectGetResponses = {
|
||||
/**
|
||||
* Successful Response
|
||||
*/
|
||||
200: unknown;
|
||||
};
|
||||
|
||||
export type BrowseArchiveIndexInventoryBrowseGetData = {
|
||||
body?: never;
|
||||
path?: never;
|
||||
@@ -1539,7 +1784,7 @@ export type ListArchivalHistoryBackupsGetResponses = {
|
||||
*
|
||||
* Successful Response
|
||||
*/
|
||||
200: Array<BackupJobSchema>;
|
||||
200: Array<AppApiBackupsJobSchema>;
|
||||
};
|
||||
|
||||
export type ListArchivalHistoryBackupsGetResponse = ListArchivalHistoryBackupsGetResponses[keyof ListArchivalHistoryBackupsGetResponses];
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { X, Activity, Search, Play, RotateCw, Clock, CheckCircle2, AlertCircle, FileText, Database, HardDrive, MapPin, ExternalLink, ArrowRight } from 'lucide-svelte';
|
||||
import { X, Activity, Search, Play, RotateCw, Clock, CheckCircle2, AlertCircle, FileText, Database, HardDrive, MapPin, ExternalLink, ArrowRight, Terminal } from 'lucide-svelte';
|
||||
import { Button } from './ui/button';
|
||||
import { Card } from './ui/card';
|
||||
import Dialog from './ui/Dialog.svelte';
|
||||
import { getJobDetailSystemJobsJobIdGet, type JobSchema } from '$lib/api';
|
||||
import { getJobDetailSystemJobsJobIdGet, getJobLogsSystemJobsJobIdLogsGet, type AppApiSystemJobSchema } from '$lib/api';
|
||||
import { cn, formatLocalTime, formatLocalDateTime, parseUTCDate } from '$lib/utils';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
@@ -12,16 +12,19 @@
|
||||
onClear: () => void;
|
||||
}>();
|
||||
|
||||
let job = $state<JobSchema | null>(null);
|
||||
let job = $state<AppApiSystemJobSchema | null>(null);
|
||||
let logs = $state<{ id: number; message: string; timestamp: string }[]>([]);
|
||||
let loading = $state(true);
|
||||
|
||||
async function loadJob() {
|
||||
loading = true;
|
||||
try {
|
||||
const response = await getJobDetailSystemJobsJobIdGet({
|
||||
path: { job_id: jobId }
|
||||
});
|
||||
if (response.data) job = response.data;
|
||||
const [jobRes, logsRes] = await Promise.all([
|
||||
getJobDetailSystemJobsJobIdGet({ path: { job_id: jobId } }),
|
||||
getJobLogsSystemJobsJobIdLogsGet({ path: { job_id: jobId } })
|
||||
]);
|
||||
if (jobRes.data) job = jobRes.data;
|
||||
if (logsRes.data) logs = logsRes.data;
|
||||
} catch (error) {
|
||||
console.error("Failed to load job details:", error);
|
||||
} finally {
|
||||
@@ -42,6 +45,12 @@
|
||||
return `${minutes}m ${remSeconds}s`;
|
||||
}
|
||||
|
||||
function formatLogTime(timestamp: string) {
|
||||
const date = parseUTCDate(timestamp);
|
||||
if (!date) return '--';
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
onMount(loadJob);
|
||||
</script>
|
||||
|
||||
@@ -99,18 +108,26 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Final Status / Logs -->
|
||||
<!-- Execution Log -->
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2 px-1">
|
||||
<FileText size={14} class="text-text-secondary opacity-50" />
|
||||
<Terminal size={14} class="text-text-secondary opacity-50" />
|
||||
<h3 class="text-[10px] font-medium text-text-secondary uppercase tracking-wider">Execution log</h3>
|
||||
</div>
|
||||
|
||||
<div class={cn(
|
||||
"p-5 rounded-xl border mono text-xs leading-relaxed",
|
||||
job.status === 'FAILED' ? "bg-error-color/5 border-error-color/20 text-error-color/90" : "bg-bg-primary border-border-color/60 text-text-primary/80"
|
||||
)}>
|
||||
{#if job.error_message}
|
||||
{#if logs.length > 0}
|
||||
<div class="bg-bg-primary border border-border-color/60 rounded-xl overflow-hidden">
|
||||
<div class="max-h-[300px] overflow-y-auto font-mono text-xs p-4 space-y-1">
|
||||
{#each logs as log (log.id)}
|
||||
<div class="flex gap-3 leading-relaxed">
|
||||
<span class="text-text-secondary/40 shrink-0 select-none">{formatLogTime(log.timestamp)}</span>
|
||||
<span class="text-text-primary/80 whitespace-pre-wrap break-words">{log.message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else if job.error_message}
|
||||
<div class="p-5 rounded-xl border mono text-xs leading-relaxed bg-error-color/5 border-error-color/20 text-error-color/90">
|
||||
<div class="flex gap-3 items-start">
|
||||
<AlertCircle size={16} class="shrink-0 mt-0.5" />
|
||||
<div>
|
||||
@@ -118,7 +135,9 @@
|
||||
{job.error_message}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="p-5 rounded-xl border mono text-xs leading-relaxed bg-bg-primary border-border-color/60 text-text-primary/80">
|
||||
<div class="flex gap-3 items-start text-success-color">
|
||||
<CheckCircle2 size={16} class="shrink-0 mt-0.5" />
|
||||
<div>
|
||||
@@ -126,8 +145,8 @@
|
||||
{job.current_task || 'Process completed successfully with zero hardware interrupts.'}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Next Steps / Metadata -->
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { RotateCw, Activity } from 'lucide-svelte';
|
||||
import { RotateCw, Activity, CheckCircle2 } from 'lucide-svelte';
|
||||
import { Card } from '$lib/components/ui/card';
|
||||
import { getScanStatusSystemScanStatusGet, type ScanStatusSchema } from '$lib/api';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let scanStatus = $state<ScanStatusSchema | null>(null);
|
||||
let pollInterval: any;
|
||||
let showCompleted = $state(false);
|
||||
let completedTimeout: any;
|
||||
|
||||
async function updateScanStatus() {
|
||||
try {
|
||||
@@ -16,6 +19,9 @@
|
||||
|
||||
if (wasRunning && !scanStatus.is_running) {
|
||||
toast.success("Filesystem scan completed");
|
||||
showCompleted = true;
|
||||
if (completedTimeout) clearTimeout(completedTimeout);
|
||||
completedTimeout = setTimeout(() => { showCompleted = false; }, 5000);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -30,6 +36,7 @@
|
||||
|
||||
onDestroy(() => {
|
||||
if (pollInterval) clearInterval(pollInterval);
|
||||
if (completedTimeout) clearTimeout(completedTimeout);
|
||||
});
|
||||
|
||||
const scanProgress = $derived(
|
||||
@@ -40,59 +47,82 @@
|
||||
</script>
|
||||
|
||||
{#if scanStatus?.is_running}
|
||||
<div class="fixed bottom-8 right-8 z-[100] bg-bg-secondary border border-blue-500/30 rounded-xl p-6 shadow-[0_25px_60px_rgba(0,0,0,0.6)] w-[450px] animate-in fade-in slide-in-from-bottom-8 border-l-4 border-l-blue-500 overflow-hidden group">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-500/5 to-transparent pointer-events-none"></div>
|
||||
|
||||
<div class="relative z-10">
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="p-3 bg-blue-500/10 rounded-xl border border-blue-500/20 group-hover:scale-110 transition-transform duration-500">
|
||||
<RotateCw size={24} class="animate-spin text-blue-500" />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<div class="flex justify-between items-center mb-1">
|
||||
<span class="text-xs font-black uppercase tracking-widest text-text-primary">System Scanner Active</span>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="fixed bottom-6 right-6 z-[100] w-[420px] animate-in fade-in slide-in-from-bottom-4" data-testid="scan-status-overlay">
|
||||
<Card class="bg-bg-secondary border-border-color shadow-2xl overflow-hidden">
|
||||
<!-- Header -->
|
||||
<header class="px-5 py-4 border-b border-border-color bg-bg-tertiary/30 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-blue-500/5 to-transparent pointer-events-none"></div>
|
||||
<div class="flex items-center gap-4 relative z-10">
|
||||
<div class="p-2.5 bg-blue-500/10 rounded-xl text-blue-500 border border-blue-500/20">
|
||||
<RotateCw size={20} class="animate-spin" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-semibold text-text-primary">Filesystem scan in progress</h3>
|
||||
{#if scanStatus.is_throttled}
|
||||
<span class="text-4xs font-black bg-orange-500/10 text-orange-500 px-2 py-0.5 rounded border border-orange-500/20 animate-pulse">THROTTLED</span>
|
||||
<span class="text-[10px] font-medium bg-orange-500/10 text-orange-500 px-2 py-0.5 rounded border border-orange-500/20 ml-2 shrink-0">THROTTLED</span>
|
||||
{/if}
|
||||
<span class="text-sm font-black mono text-blue-400">{scanStatus.hashing_speed}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse"></div>
|
||||
<p class="text-3xs font-bold uppercase tracking-[0.2em] text-text-secondary opacity-60">
|
||||
New: {scanStatus.files_new} • Mod: {scanStatus.files_modified}
|
||||
<p class="text-xs text-text-secondary mt-0.5">
|
||||
{scanStatus.files_processed.toLocaleString()} of {scanStatus.total_files_found.toLocaleString()} files
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Progress -->
|
||||
<div class="p-5 space-y-4">
|
||||
<div class="w-full bg-bg-primary h-2 rounded-full overflow-hidden">
|
||||
<div
|
||||
class="bg-blue-500 h-full transition-all duration-1000"
|
||||
style="width: {scanProgress}%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="bg-bg-primary/50 rounded-lg px-3 py-2.5 border border-border-color/50">
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase tracking-wide">New files</p>
|
||||
<p class="text-sm font-semibold mono text-text-primary mt-0.5">{scanStatus.files_new.toLocaleString()}</p>
|
||||
</div>
|
||||
<div class="bg-bg-primary/50 rounded-lg px-3 py-2.5 border border-border-color/50">
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase tracking-wide">Modified</p>
|
||||
<p class="text-sm font-semibold mono text-text-primary mt-0.5">{scanStatus.files_modified.toLocaleString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if scanStatus.current_path}
|
||||
<div class="bg-bg-primary/50 rounded-lg px-3 py-2.5 border border-border-color/50">
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase tracking-wide mb-1">Current file</p>
|
||||
<p class="text-xs text-text-primary mono truncate">{scanStatus.current_path}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if scanStatus.hashing_speed}
|
||||
<div class="flex items-center gap-2 text-xs text-text-secondary">
|
||||
<Activity size={12} />
|
||||
<span class="mono">{scanStatus.hashing_speed}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="flex justify-between items-center text-3xs font-black uppercase tracking-widest text-text-secondary">
|
||||
<span class="flex items-center gap-2">
|
||||
<Activity size={12} class="opacity-50" />
|
||||
Indexing Data
|
||||
</span>
|
||||
<span class="mono text-text-primary">
|
||||
{scanStatus.files_processed.toLocaleString()} / {scanStatus.total_files_found.toLocaleString()}
|
||||
</span>
|
||||
</Card>
|
||||
</div>
|
||||
{:else if showCompleted}
|
||||
<div class="fixed bottom-6 right-6 z-[100] w-[420px] animate-in fade-in slide-in-from-bottom-4">
|
||||
<Card class="bg-bg-secondary border-border-color shadow-2xl overflow-hidden">
|
||||
<header class="px-5 py-4 border-b border-border-color bg-bg-tertiary/30 relative overflow-hidden">
|
||||
<div class="absolute inset-0 bg-gradient-to-r from-green-500/5 to-transparent pointer-events-none"></div>
|
||||
<div class="flex items-center gap-4 relative z-10">
|
||||
<div class="p-2.5 bg-green-500/10 rounded-xl text-green-500 border border-green-500/20">
|
||||
<CheckCircle2 size={20} />
|
||||
</div>
|
||||
|
||||
<div class="w-full bg-bg-primary h-1.5 rounded-full border border-white/5 overflow-hidden">
|
||||
<div
|
||||
class="bg-blue-500 h-full transition-all duration-1000 shadow-[0_0_10px_rgba(59,130,246,0.4)]"
|
||||
style="width: {scanProgress}%"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div class="bg-bg-primary/80 px-4 py-2.5 rounded-lg border border-white/5 shadow-inner">
|
||||
<p class="text-3xs text-blue-300/80 truncate mono italic leading-relaxed">
|
||||
{scanStatus.current_path || 'Starting scan...'}
|
||||
<div class="flex-1">
|
||||
<h3 class="text-sm font-semibold text-text-primary">Scan completed</h3>
|
||||
<p class="text-xs text-text-secondary mt-0.5">
|
||||
{scanStatus!.files_processed.toLocaleString()} files indexed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</Card>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -113,10 +113,23 @@
|
||||
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
||||
}
|
||||
|
||||
function formatPath(path: string, maxLength = 70) {
|
||||
if (path.length <= maxLength) return { head: path, tail: null };
|
||||
const parts = path.split('/');
|
||||
if (parts.length <= 3) return { head: path, tail: null };
|
||||
|
||||
const headParts = parts.slice(0, -2);
|
||||
const tailParts = parts.slice(-2);
|
||||
const head = headParts.join('/');
|
||||
const tail = tailParts.join('/');
|
||||
|
||||
return { head, tail };
|
||||
}
|
||||
|
||||
onMount(loadDiscrepancies);
|
||||
|
||||
const deletedItems = $derived(discrepancies.filter(d => d.is_deleted));
|
||||
const missingItems = $derived(discrepancies.filter(d => !d.is_deleted));
|
||||
const missingItems = $derived(discrepancies.filter(d => d.is_deleted));
|
||||
const pendingItems = $derived(discrepancies.filter(d => !d.is_deleted));
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -161,9 +174,9 @@
|
||||
<FileX size={20} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-xs text-text-secondary opacity-60 block mb-1">Confirmed deleted</span>
|
||||
<h4 class="text-2xl font-bold text-error-color mono tabular-nums">{deletedItems.length}</h4>
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40 mt-1">Files marked as removed from disk</p>
|
||||
<span class="text-xs text-text-secondary opacity-60 block mb-1">Missing from disk</span>
|
||||
<h4 class="text-2xl font-bold text-error-color mono tabular-nums">{missingItems.length}</h4>
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40 mt-1">Files the scanner did not find</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -174,9 +187,9 @@
|
||||
<FileQuestion size={20} />
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<span class="text-xs text-text-secondary opacity-60 block mb-1">Missing from disk</span>
|
||||
<h4 class="text-2xl font-bold text-yellow-500 mono tabular-nums">{missingItems.length}</h4>
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40 mt-1">Tracked files not found during scan</p>
|
||||
<span class="text-xs text-text-secondary opacity-60 block mb-1">Pending confirmation</span>
|
||||
<h4 class="text-2xl font-bold text-yellow-500 mono tabular-nums">{pendingItems.length}</h4>
|
||||
<p class="text-[10px] font-medium text-text-secondary uppercase opacity-40 mt-1">Tracked files not yet confirmed</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -198,28 +211,37 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each discrepancies as item (item.id)}
|
||||
{@const path = formatPath(item.path)}
|
||||
<tr class="border-b border-border-color/10 hover:bg-white/[0.02] transition-colors group">
|
||||
<td class="px-6 py-4">
|
||||
<td class="px-6 py-4 align-top">
|
||||
{#if item.is_deleted}
|
||||
<StatusBadge variant="error">Deleted</StatusBadge>
|
||||
<StatusBadge variant="error">Missing</StatusBadge>
|
||||
{:else}
|
||||
<StatusBadge variant="warning">Missing</StatusBadge>
|
||||
<StatusBadge variant="warning">Pending</StatusBadge>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="max-w-md">
|
||||
<span class="text-sm font-medium text-text-primary mono truncate block" title={item.path}>
|
||||
{item.path.split('/').pop()}
|
||||
</span>
|
||||
<span class="text-[10px] text-text-secondary opacity-40 mono truncate block" title={item.path}>
|
||||
{item.path}
|
||||
</span>
|
||||
<td class="px-6 py-4 align-top">
|
||||
<div class="max-w-[500px]">
|
||||
{#if path.tail}
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<span class="text-xs font-medium text-text-secondary mono leading-tight" title={item.path}>
|
||||
{path.head}
|
||||
</span>
|
||||
<span class="text-sm font-medium text-text-primary mono leading-tight" title={item.path}>
|
||||
{path.tail}
|
||||
</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="text-sm font-medium text-text-primary mono truncate block" title={item.path}>
|
||||
{path.head}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<td class="px-6 py-4 text-right align-top">
|
||||
<span class="text-xs text-text-secondary mono">{formatSize(item.size)}</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<td class="px-6 py-4 align-top">
|
||||
<span class="text-xs text-text-secondary mono">
|
||||
{#if item.last_seen_timestamp}
|
||||
{formatLocalDate(item.last_seen_timestamp)}
|
||||
@@ -228,7 +250,7 @@
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-center">
|
||||
<td class="px-6 py-4 text-center align-top">
|
||||
{#if item.has_versions}
|
||||
<div class="inline-flex items-center gap-1.5 text-success-color">
|
||||
<ShieldCheck size={14} />
|
||||
@@ -241,8 +263,8 @@
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<td class="px-6 py-4 align-top">
|
||||
<div class="flex items-center justify-end gap-2 pt-1">
|
||||
{#if item.is_deleted}
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -15,13 +15,14 @@
|
||||
type ScanStatusSchema
|
||||
} from '$lib/api';
|
||||
import { toast } from "svelte-sonner";
|
||||
import { cn } from "$lib/utils";
|
||||
import { cn, formatLocalTime } from "$lib/utils";
|
||||
import { page } from '$app/state';
|
||||
|
||||
// Current directory state
|
||||
let currentPath = $state('ROOT');
|
||||
let searchQuery = $state('');
|
||||
let files = $state<FileItem[]>([]);
|
||||
let lastScanTime = $state<string | null>(null);
|
||||
let loading = $state(false);
|
||||
let searchLoading = $state(false);
|
||||
let committing = $state(false);
|
||||
@@ -35,14 +36,14 @@
|
||||
let pendingChanges = $state<Map<string, boolean>>(new Map());
|
||||
|
||||
async function loadFiles(path: string) {
|
||||
if (searchQuery.trim().length >= 3) return; // Prevent loading path if searching
|
||||
if (searchQuery.trim().length >= 3) return;
|
||||
loading = true;
|
||||
try {
|
||||
const response = await browseSystemPathSystemBrowseGet({
|
||||
query: { path }
|
||||
});
|
||||
if (response.data) {
|
||||
files = response.data.map((f: any) => ({
|
||||
files = response.data.files.map((f: any) => ({
|
||||
name: f.name,
|
||||
path: f.path,
|
||||
type: f.type as 'file' | 'directory' | 'link',
|
||||
@@ -51,6 +52,7 @@
|
||||
ignored: f.ignored ?? false,
|
||||
sha256_hash: f.sha256_hash ?? null
|
||||
}));
|
||||
lastScanTime = response.data.last_scan_time ?? null;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load files:", error);
|
||||
@@ -193,6 +195,9 @@
|
||||
}
|
||||
|
||||
const hasChanges = $derived(pendingChanges.size > 0);
|
||||
const lastScanDisplay = $derived(
|
||||
lastScanTime ? `Last scanned: ${formatLocalTime(lastScanTime)}` : 'Never scanned'
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -201,8 +206,8 @@
|
||||
|
||||
<div class="flex flex-col gap-6 h-full animate-in fade-in duration-700">
|
||||
<PageHeader
|
||||
title="Live filesystem"
|
||||
description="Define backup rules & browse physical storage"
|
||||
title="Indexed filesystem"
|
||||
description={lastScanDisplay}
|
||||
icon={FolderTree}
|
||||
>
|
||||
{#snippet actions()}
|
||||
|
||||
@@ -63,7 +63,9 @@
|
||||
let showRegisterDialog = $state(false);
|
||||
let editingMedia = $state<MediaSchema | null>(null);
|
||||
|
||||
let activeMedia = $derived(mediaList.filter(m => m.status === 'active' && (m.capacity === 0 || (m.bytes_used / m.capacity) < 0.98)));
|
||||
let activeMedia = $derived(mediaList.filter(m => m.status === 'active'));
|
||||
let fullMedia = $derived(mediaList.filter(m => m.status === 'full'));
|
||||
let unavailableMedia = $derived(mediaList.filter(m => ['failed', 'retired', 'offline'].includes(m.status)));
|
||||
|
||||
// New Media Form State
|
||||
let newMedia = $state({
|
||||
@@ -734,7 +736,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- Active Media -->
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4" data-testid="active-media-section">
|
||||
<SectionHeader title="Active archive media" icon={Database} iconColor="text-blue-500" />
|
||||
|
||||
<Card class="bg-bg-secondary border-border-color shadow-2xl overflow-hidden flex flex-col">
|
||||
@@ -790,8 +792,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Fully Utilized Media -->
|
||||
{#if mediaList.some(m => m.status === 'active' && m.capacity > 0 && (m.bytes_used / m.capacity) >= 0.98)}
|
||||
<div class="space-y-4">
|
||||
{#if fullMedia.length > 0}
|
||||
<div class="space-y-4" data-testid="full-media-section">
|
||||
<SectionHeader title="Fully utilized media" icon={ShieldCheck} iconColor="text-success-color" />
|
||||
|
||||
<Card class="bg-bg-secondary/80 border border-border-color/80 rounded-xl overflow-hidden shadow-xl">
|
||||
@@ -809,7 +811,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border-color/30">
|
||||
{#each mediaList.filter(m => m.status === 'active' && m.capacity > 0 && (m.bytes_used / m.capacity) >= 0.98) as media (media.id)}
|
||||
{#each fullMedia as media (media.id)}
|
||||
<tr class="hover:bg-bg-primary/20 transition-colors">
|
||||
<td class="px-6 py-4 text-center opacity-30">
|
||||
<Minus size={16} />
|
||||
@@ -824,8 +826,8 @@
|
||||
{/if}
|
||||
|
||||
<!-- Retired & Failed Media -->
|
||||
{#if mediaList.some(m => m.status !== 'active')}
|
||||
<div class="space-y-4">
|
||||
{#if unavailableMedia.length > 0}
|
||||
<div class="space-y-4" data-testid="unavailable-media-section">
|
||||
<SectionHeader title="Retired & failed media" icon={ShieldAlert} iconColor="text-error-color" />
|
||||
|
||||
<Card class="bg-bg-secondary/60 border border-border-color/60 rounded-xl overflow-hidden shadow-xl grayscale-[0.5] opacity-80">
|
||||
@@ -843,7 +845,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border-color/20">
|
||||
{#each mediaList.filter(m => m.status !== 'active') as media (media.id)}
|
||||
{#each unavailableMedia as media (media.id)}
|
||||
<tr class="hover:bg-bg-primary/20 transition-colors">
|
||||
<td class="px-6 py-4 text-center opacity-20">
|
||||
<Minus size={16} />
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
listJobsSystemJobsGet,
|
||||
getJobsCountSystemJobsCountGet,
|
||||
cancelJobSystemJobsJobIdCancelPost,
|
||||
type JobSchema
|
||||
type AppApiSystemJobSchema
|
||||
} from '$lib/api';
|
||||
import { cn, formatLocalTime, parseUTCDate } from '$lib/utils';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let jobs = $state<JobSchema[]>([]);
|
||||
let jobs = $state<AppApiSystemJobSchema[]>([]);
|
||||
let totalJobs = $state(0);
|
||||
let loading = $state(true);
|
||||
let loadingMore = $state(false);
|
||||
@@ -197,7 +197,7 @@
|
||||
<div class="flex-1 space-y-2.5">
|
||||
<div class="flex justify-between items-end">
|
||||
<span class="text-xs font-medium text-text-secondary truncate max-w-[400px]">
|
||||
{job.current_task || 'Starting task...'}
|
||||
{job.latest_log || job.current_task || 'Starting task...'}
|
||||
</span>
|
||||
<span class="text-xs font-semibold mono text-text-primary">{job.progress.toFixed(1)}%</span>
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@
|
||||
<span class="text-sm font-semibold text-text-primary uppercase tracking-tight">{job.job_type} JOB #{job.id}</span>
|
||||
<StatusBadge variant={getStatusVariant(job.status)}>{job.status}</StatusBadge>
|
||||
</div>
|
||||
<p class="text-xs text-text-secondary mt-1 opacity-60 truncate">{job.error_message || job.current_task || 'Finished successfully'}</p>
|
||||
<p class="text-xs text-text-secondary mt-1 opacity-60 truncate">{job.latest_log || job.error_message || job.current_task || 'Finished successfully'}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-12 shrink-0">
|
||||
|
||||
@@ -128,4 +128,60 @@ test.describe('Media Lifecycle', () => {
|
||||
|
||||
await requestContext.dispose();
|
||||
});
|
||||
|
||||
test('inventory categorizes media by status correctly', async ({ page }) => {
|
||||
const requestContext = await setupRequestContext();
|
||||
|
||||
const activeMedia = await requestContext.post(`${API_URL}/inventory/media`, {
|
||||
data: { identifier: 'CAT_ACTIVE', media_type: 'mock_lto', generation_tier: 'LTO-8', capacity: 12000, config: {} }
|
||||
}).then(r => r.json());
|
||||
|
||||
const fullMedia = await requestContext.post(`${API_URL}/inventory/media`, {
|
||||
data: { identifier: 'CAT_FULL', media_type: 'mock_lto', generation_tier: 'LTO-8', capacity: 12000, config: {} }
|
||||
}).then(r => r.json());
|
||||
await requestContext.patch(`${API_URL}/inventory/media/${fullMedia.id}`, { data: { status: 'full' } });
|
||||
|
||||
const failedMedia = await requestContext.post(`${API_URL}/inventory/media`, {
|
||||
data: { identifier: 'CAT_FAILED', media_type: 'mock_lto', generation_tier: 'LTO-8', capacity: 12000, config: {} }
|
||||
}).then(r => r.json());
|
||||
await requestContext.patch(`${API_URL}/inventory/media/${failedMedia.id}`, { data: { status: 'failed' } });
|
||||
|
||||
const retiredMedia = await requestContext.post(`${API_URL}/inventory/media`, {
|
||||
data: { identifier: 'CAT_RETIRED', media_type: 'mock_lto', generation_tier: 'LTO-8', capacity: 12000, config: {} }
|
||||
}).then(r => r.json());
|
||||
await requestContext.patch(`${API_URL}/inventory/media/${retiredMedia.id}`, { data: { status: 'retired' } });
|
||||
|
||||
await page.goto('/inventory');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const activeSection = page.getByTestId('active-media-section');
|
||||
const fullSection = page.getByTestId('full-media-section');
|
||||
const unavailableSection = page.getByTestId('unavailable-media-section');
|
||||
|
||||
await expect(activeSection).toBeVisible();
|
||||
await expect(activeSection.getByText('CAT_ACTIVE')).toBeVisible();
|
||||
await expect(activeSection.getByText('CAT_FULL')).not.toBeVisible();
|
||||
await expect(activeSection.getByText('CAT_FAILED')).not.toBeVisible();
|
||||
await expect(activeSection.getByText('CAT_RETIRED')).not.toBeVisible();
|
||||
|
||||
await expect(fullSection).toBeVisible();
|
||||
await expect(fullSection.getByText('CAT_FULL')).toBeVisible();
|
||||
await expect(fullSection.getByText('CAT_ACTIVE')).not.toBeVisible();
|
||||
await expect(fullSection.getByText('CAT_FAILED')).not.toBeVisible();
|
||||
await expect(fullSection.getByText('CAT_RETIRED')).not.toBeVisible();
|
||||
|
||||
await expect(unavailableSection).toBeVisible();
|
||||
await expect(unavailableSection.getByText('CAT_FAILED')).toBeVisible();
|
||||
await expect(unavailableSection.getByText('CAT_RETIRED')).toBeVisible();
|
||||
|
||||
await expect(unavailableSection).toBeVisible();
|
||||
await expect(unavailableSection.getByText('CAT_FAILED')).toBeVisible();
|
||||
await expect(unavailableSection.getByText('CAT_RETIRED')).toBeVisible();
|
||||
|
||||
await requestContext.delete(`${API_URL}/inventory/media/${activeMedia.id}`);
|
||||
await requestContext.delete(`${API_URL}/inventory/media/${fullMedia.id}`);
|
||||
await requestContext.delete(`${API_URL}/inventory/media/${failedMedia.id}`);
|
||||
await requestContext.delete(`${API_URL}/inventory/media/${retiredMedia.id}`);
|
||||
await requestContext.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user