Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 39 additions & 7 deletions frontend/src/lib/components/workstation/HomeWorkbenchView.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,16 @@
dashboard,
foldersPayload,
hosts,
foldersPending = false,
loadError = '',
crumb = '/',
mode = 'queue'
}: {
dashboard: DashboardSummaryPayload;
foldersPayload: DashboardFoldersPayload;
hosts: HostsPayload;
foldersPending?: boolean;
loadError?: string;
crumb?: string;
mode?: 'queue' | 'folders';
} = $props();
Expand Down Expand Up @@ -133,10 +137,12 @@
dashboard,
visibleFoldersPayload,
hosts,
isFolderIndex ? folderScopeLabel : 'Work folders'
isFolderIndex ? folderScopeLabel : 'Work folders',
foldersPending
)
);
const footerSignals = $derived(buildQueueFooterSignals(dashboard, visibleFoldersPayload, hosts));
const hasLoadError = $derived(loadError.trim().length > 0);
const runningSamples = $derived(dashboard.calibration_queue.sample.running_count);
const queuedSamples = $derived(dashboard.calibration_queue.sample.queued_count);
const pendingReviews = $derived(dashboard.calibration_queue.sample.pending_review_count);
Expand Down Expand Up @@ -178,9 +184,20 @@
isFolderIndex ? `Matching ${folderScopeLabel.toLowerCase()}` : 'Workflow lanes'
);
const visibleScopeSummary = $derived(
`${visibleFolders.length.toLocaleString('en-US')} / ${folders.length.toLocaleString(
'en-US'
)} ${isFolderIndex ? folderScopeLabel.toLowerCase() : 'folders'}`
foldersPending && !isFolderIndex
? 'Loading work folders'
: `${visibleFolders.length.toLocaleString('en-US')} / ${folders.length.toLocaleString(
'en-US'
)} ${isFolderIndex ? folderScopeLabel.toLowerCase() : 'folders'}`
);
const visibleFoldersCopy = $derived(
foldersPending && !isFolderIndex ? 'loading' : visibleFolders.length.toLocaleString('en-US')
);
const visiblePendingCopy = $derived(
foldersPending && !isFolderIndex ? 'loading' : visiblePendingItems.toLocaleString('en-US')
);
const visibleReclaimCopy = $derived(
foldersPending && !isFolderIndex ? 'loading' : formatBytes(visibleProjectedReclaim)
);

type LibraryOption = {
Expand Down Expand Up @@ -499,6 +516,9 @@
<p>{headerCopy}</p>
</div>
<div class="queue-header__tools">
{#if hasLoadError}
<div class="load-error" role="status">{loadError}</div>
{/if}
{#if isFolderIndex}
<div class="scope-switch" aria-label="Folder scope">
<span>Scope</span>
Expand Down Expand Up @@ -531,15 +551,15 @@
<div class="queue-header__facts">
<div>
<span>{isFolderIndex ? `Visible ${folderScopeLabel}` : 'Visible folders'}</span>
<strong>{visibleFolders.length.toLocaleString('en-US')}</strong>
<strong>{visibleFoldersCopy}</strong>
</div>
<div>
<span>Open work</span>
<strong>{visiblePendingItems.toLocaleString('en-US')}</strong>
<strong>{visiblePendingCopy}</strong>
</div>
<div>
<span>Projected reclaim</span>
<strong>{formatBytes(visibleProjectedReclaim)}</strong>
<strong>{visibleReclaimCopy}</strong>
</div>
</div>
</div>
Expand Down Expand Up @@ -720,6 +740,10 @@
</td>
</tr>
{/each}
{:else if foldersPending}
<tr>
<td colspan="8">Loading folder worklist...</td>
</tr>
{:else}
<tr>
<td colspan="8">No folders match the current filters.</td>
Expand Down Expand Up @@ -802,6 +826,14 @@
min-width: 0;
}

.load-error {
background: var(--mf-fail-bg);
border-left: 2px solid var(--mf-fail-fg);
color: var(--mf-fail-fg);
font-size: var(--mf-text-sm);
padding: var(--mf-space-3) var(--mf-space-4);
}

.queue-header__facts {
display: grid;
gap: var(--mf-space-4);
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/lib/components/workstation/queue-workstation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ export function buildQueueStatusTiles(
dashboard: DashboardSummaryPayload,
foldersPayload: DashboardFoldersPayload,
hosts: HostsPayload,
scopeLabel = 'folders'
scopeLabel = 'folders',
foldersPending = false
): StatusTile[] {
const folders = foldersPayload.folders;
const visibleScopeLabel = scopeLabel.trim().toLowerCase() || 'folders';
Expand All @@ -233,15 +234,19 @@ export function buildQueueStatusTiles(
return [
{
label: 'Next work',
value: `${folders.length} ${visibleScopeLabel}`,
detail: `${totalPendingItems(folders).toLocaleString('en-US')} open work`,
tone: folders.length ? 'wait' : 'idle'
value: foldersPending ? 'loading worklist' : `${folders.length} ${visibleScopeLabel}`,
detail: foldersPending
? 'folder rows hydrating'
: `${totalPendingItems(folders).toLocaleString('en-US')} open work`,
tone: foldersPending ? 'active' : folders.length ? 'wait' : 'idle'
},
{
label: 'Projected reclaim',
value: formatBytes(projectedReclaim),
detail: `estimated from visible ${visibleScopeLabel}`,
tone: projectedReclaim > 0 ? 'ready' : 'idle',
value: foldersPending ? 'loading' : formatBytes(projectedReclaim),
detail: foldersPending
? `estimated after ${visibleScopeLabel} load`
: `estimated from visible ${visibleScopeLabel}`,
tone: foldersPending ? 'active' : projectedReclaim > 0 ? 'ready' : 'idle',
mono: true
},
{
Expand Down
74 changes: 64 additions & 10 deletions frontend/src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,92 @@
HostsPayload
} from '$lib/api/types';
import HomeWorkbenchView from '$lib/components/workstation/HomeWorkbenchView.svelte';
import RouteLoadingView from '$lib/components/workstation/RouteLoadingView.svelte';

let dashboard = $state<DashboardSummaryPayload | null>(null);
let foldersPayload = $state<DashboardFoldersPayload | null>(null);
let hosts = $state<HostsPayload | null>(null);
const emptyQueueLane = {
running: [],
queued: [],
pending_review: [],
running_count: 0,
queued_count: 0,
pending_review_count: 0
};

const initialDashboard: DashboardSummaryPayload = {
folders_preview: [],
library_colors: {},
scan_job: null,
calibration_queue: {
sample: emptyQueueLane,
full: emptyQueueLane,
active_count: 0
},
encode_queue: {
running_count: 0,
queued_count: 0,
running: [],
queued: [],
state: { is_paused: false, stop_requested: false, scheduler_summary: 'loading' }
},
catalog_empty: false,
folder_cache_key: 'loading',
metric_support: { vmaf: false, xpsnr: false, ssim: false },
metric_status_copy: 'loading'
};

const initialFoldersPayload: DashboardFoldersPayload = {
folders: [],
folder_cache_key: 'loading',
catalog_empty: false
};

const initialHosts: HostsPayload = { compact: true, hosts: [] };

let dashboard = $state<DashboardSummaryPayload>(initialDashboard);
let foldersPayload = $state<DashboardFoldersPayload>(initialFoldersPayload);
let hosts = $state<HostsPayload>(initialHosts);
let loadError = $state('');
let foldersPending = $state(true);

onMount(async () => {
try {
const [dashboardPayload, hostsPayload] = await Promise.all([
fetchJson<DashboardSummaryPayload>('/api/dashboard'),
fetchJson<DashboardSummaryPayload>('/api/dashboard?preview_limit=0'),
fetchJson<HostsPayload>('/api/hosts?compact=1')
]);
dashboard = dashboardPayload;
hosts = hostsPayload;
foldersPayload = {
folders: dashboardPayload.folders_preview,
folders: [],
folder_cache_key: dashboardPayload.folder_cache_key,
catalog_empty: dashboardPayload.catalog_empty
catalog_empty: false
};
foldersPending = true;
void hydrateFolders(dashboardPayload);
} catch (error) {
loadError = error instanceof Error ? error.message : 'Work route failed to load.';
foldersPending = false;
}
});

async function hydrateFolders(dashboardPayload: DashboardSummaryPayload) {
try {
const hydratedFolders = await fetchJson<DashboardFoldersPayload>(
'/api/dashboard/folders?include_series=0'
);
foldersPayload = hydratedFolders;
dashboard = { ...dashboardPayload, folders_preview: hydratedFolders.folders };
} catch (error) {
loadError = error instanceof Error ? error.message : 'Work folders failed to load.';
} finally {
foldersPending = false;
}
}
</script>

<svelte:head>
<title>Mediaforce Work</title>
</svelte:head>

{#if dashboard && foldersPayload && hosts}
<HomeWorkbenchView {dashboard} {foldersPayload} {hosts} />
{:else}
<RouteLoadingView route="queue" subject="Work" crumb="/" error={loadError} />
<HomeWorkbenchView {dashboard} {foldersPayload} {hosts} {foldersPending} {loadError} />
{/if}
4 changes: 2 additions & 2 deletions frontend/src/routes/folders/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
onMount(async () => {
try {
const [dashboardPayload, foldersPayloadResult, hostsPayload] = await Promise.all([
fetchJson<DashboardSummaryPayload>('/api/dashboard'),
fetchJson<DashboardSummaryPayload>('/api/dashboard?preview_limit=0'),
fetchJson<DashboardFoldersPayload>('/api/dashboard/folders'),
fetchJson<HostsPayload>('/api/hosts?compact=1')
]);
dashboard = dashboardPayload;
foldersPayload = foldersPayloadResult;
dashboard = { ...dashboardPayload, folders_preview: foldersPayloadResult.folders };
hosts = hostsPayload;
} catch (error) {
loadError = error instanceof Error ? error.message : 'Folders route failed to load.';
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/routes/ops/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
onMount(async () => {
try {
const [dashboardPayload, hostsPayload] = await Promise.all([
fetchJson<DashboardSummaryPayload>('/api/dashboard'),
fetchJson<DashboardSummaryPayload>('/api/dashboard?preview_limit=0'),
fetchJson<HostsPayload>('/api/hosts?compact=1')
]);
dashboard = dashboardPayload;
Expand Down
10 changes: 6 additions & 4 deletions mediaforce/web/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,28 +404,30 @@ def _settings_page_payload(
schedule_profiles=schedule_profiles,
)

def _dashboard_summary_payload() -> dict[str, Any]:
def _dashboard_summary_payload(preview_limit: int | None = None) -> dict[str, Any]:
return dashboard_summary_payload(
config,
folder_card_cache_key=_folder_card_cache_key,
preview_folder_cards=_preview_folder_cards,
maybe_schedule_scan=_maybe_schedule_scan,
decorate_encode_queue_for_scheduler=_decorate_encode_queue_for_scheduler,
library_color_map_for_config=_library_color_map_for_config,
preview_limit=preview_limit,
)

def _dashboard_folders_payload() -> dict[str, Any]:
def _dashboard_folders_payload(include_series_folders: bool = True) -> dict[str, Any]:
return dashboard_folders_payload(
config,
folder_card_cache_key=_folder_card_cache_key,
list_folder_cards=_list_folder_cards,
list_series_folder_cards=_list_series_folder_cards,
include_series_folders=include_series_folders,
)

def _dashboard_api_payload() -> dict[str, Any]:
def _dashboard_api_payload(preview_limit: int | None = None) -> dict[str, Any]:
metric_support = _metric_support()
return {
**_dashboard_summary_payload(),
**_dashboard_summary_payload(preview_limit),
"metric_support": dict(metric_support),
"metric_status_copy": _metric_status_copy(metric_support),
}
Expand Down
18 changes: 10 additions & 8 deletions mediaforce/web/routes/dashboard.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
from collections.abc import Callable
from typing import Any
from typing import Annotated, Any

from fastapi import FastAPI
from fastapi import FastAPI, Query
from fastapi.responses import JSONResponse

PreviewLimit = Annotated[int | None, Query(ge=0)]


def register_dashboard_routes(
app: FastAPI,
*,
dashboard_payload: Callable[[], dict[str, Any]],
dashboard_folders_payload: Callable[[], dict[str, Any]],
dashboard_payload: Callable[[int | None], dict[str, Any]],
dashboard_folders_payload: Callable[[bool], dict[str, Any]],
) -> None:
@app.get("/api/dashboard")
def api_dashboard() -> JSONResponse:
return JSONResponse(dashboard_payload())
def api_dashboard(preview_limit: PreviewLimit = None) -> JSONResponse:
return JSONResponse(dashboard_payload(preview_limit))

@app.get("/api/dashboard/folders")
def api_dashboard_folders() -> JSONResponse:
return JSONResponse(dashboard_folders_payload())
def api_dashboard_folders(include_series: int = 1) -> JSONResponse:
return JSONResponse(dashboard_folders_payload(bool(include_series)))
Loading