39726-vm/job.php
2026-04-19 01:12:47 +00:00

251 lines
12 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/app.php';
app_boot();
$jobId = trim((string)($_GET['id'] ?? ''));
$job = $jobId !== '' ? find_job($jobId) : null;
$notice = trim((string)($_GET['notice'] ?? ''));
$noticeType = trim((string)($_GET['type'] ?? 'info')) ?: 'info';
$toolLabel = $job ? job_tool_label($job) : 'Conversion';
$conversionSummary = $job ? job_conversion_summary($job) : 'Job summary';
$meta = page_meta(project_name() . ' — ' . $toolLabel . ' job', 'Review the result, status, and download link for a completed conversion job.');
$statusTitle = 'Conversion in progress';
$statusCopy = 'This job is still processing. Refresh the page in a few seconds if needed.';
if ($job) {
if (($job['status'] ?? '') === 'completed') {
$statusTitle = 'Download ready';
$statusCopy = 'The conversion finished successfully and the output file is ready for download.';
} elseif (($job['status'] ?? '') === 'failed') {
$statusTitle = 'Conversion failed';
$statusCopy = 'The job did not finish cleanly. Review the error details below and try again.';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><?= h($meta['title']) ?></title>
<meta name="description" content="<?= h($meta['description']) ?>">
<?php if ($meta['description'] !== ''): ?>
<meta property="og:description" content="<?= h($meta['description']) ?>">
<meta property="twitter:description" content="<?= h($meta['description']) ?>">
<?php endif; ?>
<?php if ($meta['image'] !== ''): ?>
<meta property="og:image" content="<?= h($meta['image']) ?>">
<meta property="twitter:image" content="<?= h($meta['image']) ?>">
<?php endif; ?>
<meta property="og:title" content="<?= h($meta['title']) ?>">
<meta property="twitter:title" content="<?= h($meta['title']) ?>">
<meta name="twitter:card" content="summary_large_image">
<script>
(() => {
try {
const savedTheme = localStorage.getItem('fs-theme');
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
const theme = savedTheme || (prefersDark ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', theme);
document.documentElement.setAttribute('data-bs-theme', theme);
} catch (error) {
document.documentElement.setAttribute('data-theme', 'light');
document.documentElement.setAttribute('data-bs-theme', 'light');
}
})();
</script>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="/assets/css/custom.css?v=<?= time() ?>">
</head>
<body data-app-state="<?= ffmpeg_is_available() ? 'ready' : 'offline' ?>" data-upload-limit-mb="<?= h((string)effective_upload_limit_mb()) ?>">
<nav class="navbar navbar-expand-lg app-nav sticky-top">
<div class="container">
<div class="d-flex align-items-center justify-content-between w-100 gap-3 flex-wrap">
<a class="navbar-brand d-flex align-items-center gap-2" href="/" aria-label="<?= h(project_name()) ?> home">
<span class="brand-mark">FS</span>
<span><?= h(project_name()) ?></span>
</a>
<div class="nav-actions d-flex align-items-center gap-2 gap-lg-3 small text-secondary">
<a class="nav-link px-0" href="/">Dashboard</a>
<a class="nav-link px-0" href="/healthz.php">Health</a>
<button class="btn btn-sm btn-soft theme-toggle" id="theme-toggle" type="button" aria-label="Toggle color theme" aria-pressed="false">
<span class="theme-toggle-icon" id="theme-toggle-icon" aria-hidden="true">🌙</span>
<span class="theme-toggle-label" id="theme-toggle-label">Dark mode</span>
</button>
</div>
</div>
</div>
</nav>
<main class="py-4 py-lg-5">
<div class="page-orb orb-left" aria-hidden="true"></div>
<div class="page-orb orb-right" aria-hidden="true"></div>
<div class="container position-relative">
<?php if ($notice !== ''): ?>
<div class="alert alert-<?= h($noticeType) ?> app-alert shadow-sm" role="alert">
<?= h($notice) ?>
</div>
<?php endif; ?>
<?php if (!$job): ?>
<section class="app-card empty-state-card">
<h1 class="detail-title">Job not found</h1>
<p class="hero-copy mb-4">The requested conversion record is missing or has already expired from the retention window.</p>
<a class="btn btn-dark" href="/">Back to dashboard</a>
</section>
<?php else: ?>
<section class="hero-shell mb-4">
<div class="row g-4 align-items-stretch">
<div class="col-xl-8">
<div class="hero-panel h-100">
<span class="eyebrow"><?= h($toolLabel) ?></span>
<h1 class="detail-title"><?= h($conversionSummary) ?></h1>
<p class="hero-copy mb-4 text-break">Original file: <strong><?= h((string)$job['original_name']) ?></strong></p>
<div class="hero-actions">
<?php if (($job['status'] ?? '') === 'completed' && job_output_exists($job)): ?>
<a class="btn btn-dark btn-lg" href="/download.php?id=<?= urlencode((string)$job['public_id']) ?>"><?= h(job_download_label($job)) ?></a>
<?php endif; ?>
<a class="btn btn-soft btn-lg" href="/">Run another conversion</a>
</div>
</div>
</div>
<div class="col-xl-4">
<aside class="app-card app-card-highlight h-100">
<div class="status-summary status-summary-<?= h((string)$job['status']) ?>">
<div class="status-summary-label">Current state</div>
<div class="status-summary-title"><?= h($statusTitle) ?></div>
<p class="mb-0"><?= h($statusCopy) ?></p>
</div>
</aside>
</div>
</div>
</section>
<div class="row g-4">
<div class="col-lg-8">
<section class="app-card mb-4">
<div class="card-header-row mb-4">
<div>
<h2 class="section-title mb-1">Job metrics</h2>
<p class="section-subtitle mb-0">A concise view of the source, output, preset, and runtime status for this job.</p>
</div>
</div>
<div class="detail-grid">
<div class="detail-item">
<span>Converter</span>
<strong><?= h($toolLabel) ?></strong>
</div>
<div class="detail-item">
<span>Formats</span>
<strong><?= h($conversionSummary) ?></strong>
</div>
<div class="detail-item">
<span>Input size</span>
<strong><?= h(format_bytes(isset($job['input_size']) ? (int)$job['input_size'] : null)) ?></strong>
</div>
<div class="detail-item">
<span>Output size</span>
<strong><?= h(format_bytes(isset($job['output_size']) ? (int)$job['output_size'] : null)) ?></strong>
</div>
<div class="detail-item">
<span>Started</span>
<strong><?= h(format_datetime((string)$job['created_at'])) ?></strong>
</div>
<div class="detail-item">
<span>Completed</span>
<strong><?= h(format_datetime((string)($job['completed_at'] ?? ''))) ?></strong>
</div>
</div>
<?php if (($job['status'] ?? '') === 'completed' && job_output_exists($job)): ?>
<div class="download-panel mt-4">
<div class="download-panel-copy">
<h2 class="section-title mb-2"><?= h(job_download_label($job)) ?></h2>
<p class="mb-0">The output file is available now. Downloads remain available until the <?= h((string)APP_RETENTION_HOURS) ?>-hour retention window expires.</p>
</div>
<div class="d-flex flex-column flex-sm-row gap-2 mt-3">
<a class="btn btn-dark" href="/download.php?id=<?= urlencode((string)$job['public_id']) ?>"><?= h(job_download_label($job)) ?></a>
<a class="btn btn-soft" href="/">Go back to dashboard</a>
</div>
</div>
<?php elseif (($job['status'] ?? '') === 'failed'): ?>
<div class="alert alert-danger mt-4 mb-0 app-alert-block">
<strong>Conversion failed.</strong>
<div class="small mt-2"><?= nl2br(h((string)($job['error_message'] ?? 'The converter did not finish successfully.'))) ?></div>
</div>
<?php else: ?>
<div class="alert alert-warning mt-4 mb-0 app-alert-block">
This job is still marked as processing. Refresh the page in a few seconds if needed.
</div>
<?php endif; ?>
</section>
<section class="workflow-grid workflow-grid-2">
<article class="app-card flow-card">
<span class="flow-step">A</span>
<h2 class="section-title mb-2">Input captured</h2>
<p class="mb-0">The source file and requested tool settings were stored before processing started, so the result can be tracked even if the run fails.</p>
</article>
<article class="app-card flow-card">
<span class="flow-step">B</span>
<h2 class="section-title mb-2">Output retained</h2>
<p class="mb-0">Generated files stay available for <?= h((string)APP_RETENTION_HOURS) ?> hours, after which cleanup removes the temporary artifacts.</p>
</article>
</section>
</div>
<div class="col-lg-4">
<aside class="app-card h-100">
<h2 class="section-title mb-3">Run details</h2>
<dl class="meta-list mb-4">
<div>
<dt>Original file</dt>
<dd class="text-break"><?= h((string)$job['original_name']) ?></dd>
</div>
<div>
<dt>Status</dt>
<dd><?= h(ucfirst((string)$job['status'])) ?></dd>
</div>
<div>
<dt>Preset</dt>
<dd><?= h(job_preset_label($job) !== '' ? job_preset_label($job) : '—') ?></dd>
</div>
<div>
<dt>Download name</dt>
<dd class="text-break"><?= h(job_download_name($job)) ?></dd>
</div>
</dl>
<div class="mini-note">
<div class="mini-note-title">Next move</div>
<ul class="mini-steps mb-0 ps-3">
<?php if (($job['status'] ?? '') === 'completed'): ?>
<li>Download the generated file while it is still within the retention window.</li>
<li>Return to the dashboard if you want to run another tool or preset.</li>
<?php elseif (($job['status'] ?? '') === 'failed'): ?>
<li>Review the error output above for clues.</li>
<li>Try a different source file, output choice, or a smaller upload.</li>
<?php else: ?>
<li>Refresh the page after a short wait.</li>
<li>If it stays stuck, start a fresh run from the dashboard.</li>
<?php endif; ?>
</ul>
</div>
</aside>
</div>
</div>
<?php endif; ?>
</div>
</main>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" defer></script>
<script src="/assets/js/main.js?v=<?= time() ?>" defer></script>
</body>
</html>