<!doctype html>
<html lang="en" data-theme="dark">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ChainMQ Dashboard</title>
<link
rel="icon"
href="favicon.svg?v=chainmq-ui-1"
type="image/svg+xml"
/>
<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=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="styles.css?v=chainmq-ui-46" />
</head>
<body>
<div
id="loginOverlay"
class="login-overlay"
hidden
role="dialog"
aria-modal="true"
aria-labelledby="loginTitle"
>
<form id="loginForm" class="login-card" autocomplete="on">
<h2 id="loginTitle" class="login-title">ChainMQ</h2>
<p class="login-subtitle">Sign in to the dashboard</p>
<label class="login-label">
<span>Username</span>
<input
id="loginUsername"
name="username"
type="text"
autocomplete="username"
value="ChainMQ"
required
class="login-input"
/>
</label>
<label class="login-label">
<span>Password</span>
<input
id="loginPassword"
name="password"
type="password"
autocomplete="current-password"
required
class="login-input"
/>
</label>
<p id="loginError" class="login-error" hidden></p>
<button type="submit" class="login-submit" id="loginSubmit">
Sign in
</button>
</form>
</div>
<div class="app-container" id="appContainer">
<div
id="sidebarBackdrop"
class="sidebar-backdrop"
hidden
aria-hidden="true"
></div>
<aside class="sidebar" id="sidebarNavDrawer" aria-label="Queues navigation">
<div class="sidebar-header">
<button
type="button"
id="sidebarCollapseBtn"
class="icon-btn sidebar-collapse-btn"
aria-expanded="true"
aria-controls="sidebarQueuesNav"
title="Collapse sidebar"
>
<svg
class="sidebar-collapse-icon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M15 18l-6-6 6-6" />
</svg>
</button>
<div class="sidebar-header-body">
<div class="logo">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" />
</svg>
<h1>ChainMQ</h1>
</div>
<div class="header-actions">
<button id="themeToggle" class="icon-btn" title="Toggle theme">
<svg
id="themeIcon"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="5" />
<path
d="M12 1v2M12 21v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M1 12h2M21 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42"
/>
</svg>
</button>
<button id="refreshBtn" class="icon-btn" title="Refresh">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.48L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
/>
</svg>
</button>
</div>
</div>
</div>
<div class="sidebar-content" id="sidebarQueuesNav">
<div class="queues-section">
<h2>Queues</h2>
<div id="queues-list" class="queues-list">
<div class="loading-queues">Loading queues...</div>
</div>
</div>
</div>
<div class="sidebar-footer">
<button
type="button"
id="logoutBtn"
class="sidebar-logout-btn"
title="Sign out"
hidden
aria-label="Sign out"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" />
<polyline points="16 17 21 12 16 7" />
<line x1="21" y1="12" x2="9" y2="12" />
</svg>
<span class="sidebar-logout-label">Sign out</span>
</button>
</div>
</aside>
<main class="main-content">
<header class="mobile-chrome" aria-label="Mobile toolbar">
<div class="mobile-chrome-brand">
<svg
class="mobile-chrome-mark"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
<span class="mobile-chrome-title">ChainMQ</span>
</div>
<button
type="button"
id="mobileNavOpenBtn"
class="icon-btn mobile-nav-open-btn"
aria-expanded="false"
aria-controls="sidebarNavDrawer"
title="Open menu"
aria-label="Open menu"
>
<svg
class="mobile-nav-open-icon"
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M4 6h16M4 12h16M4 18h16" />
</svg>
</button>
</header>
<div id="empty-state" class="empty-state-main">
<div class="empty-icon">
<svg
width="64"
height="64"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
>
<path d="M9 12l2 2 4-4" />
<path d="M21 12c-1 0-3-1-3-3s2-3 3-3 3 1 3 3-2 3-3 3" />
<path d="M3 12c1 0 3-1 3-3s-2-3-3-3-3 1-3 3 2 3 3 3" />
<path d="M12 3c0 1-1 3-3 3S6 4 6 3s1-3 3-3 3 2 3 3" />
<path d="M12 21c0-1-1-3-3-3s-3 2-3 3 1 3 3 3 3-2 3-3" />
</svg>
</div>
<h2>Select a queue to view jobs</h2>
<p>Choose a queue from the sidebar to start monitoring jobs</p>
</div>
<div id="queue-view" class="queue-view" style="display: none">
<div class="queue-header">
<div class="queue-title">
<div class="queue-title-text">
<span class="queue-eyebrow">Queue</span>
<h2 id="queue-name-display">Queue Name</h2>
</div>
<div class="queue-actions">
<button
id="processDelayedBtn"
class="btn btn-secondary btn-icon"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
Process Delayed
</button>
<button
type="button"
id="processRepeatBtn"
class="btn btn-secondary btn-icon"
title="Promote due repeat schedules"
>
Process Repeat
</button>
<button
type="button"
id="pauseQueueBtn"
class="btn btn-secondary btn-icon"
>
Pause queue
</button>
<button
type="button"
id="resumeQueueBtn"
class="btn btn-secondary btn-icon"
>
Resume queue
</button>
<button
type="button"
id="redisStatsBtn"
class="btn btn-secondary btn-icon"
title="Redis server memory and health"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<ellipse cx="12" cy="5" rx="9" ry="3" />
<path d="M3 5v7c0 1.66 4 3 9 3s9-1.34 9-3V5" />
<path d="M3 12v7c0 1.66 4 3 9 3s9-1.34 9-3v-7" />
</svg>
Redis
</button>
<button
id="recoverStalledBtn"
class="btn btn-secondary btn-icon"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 4v6h6M23 20v-6h-6" />
<path
d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"
/>
</svg>
Recover Stalled
</button>
<button id="cleanBtn" class="btn btn-secondary btn-icon">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polyline points="3 6 5 6 21 6" />
<path
d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"
/>
</svg>
Clean
</button>
</div>
</div>
</div>
<div
class="stats-cards"
role="tablist"
aria-label="Job state"
>
<div
class="stat-card active"
data-state="completed"
role="tab"
aria-selected="true"
>
<div class="stat-icon completed">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
</div>
<div class="stat-info">
<div class="stat-label">Completed</div>
<div class="stat-value" id="stat-completed">0</div>
</div>
</div>
<div
class="stat-card"
data-state="waiting"
role="tab"
aria-selected="false"
>
<div class="stat-icon waiting">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</div>
<div class="stat-info">
<div class="stat-label">Waiting</div>
<div class="stat-value" id="stat-waiting">0</div>
</div>
</div>
<div
class="stat-card"
data-state="active"
role="tab"
aria-selected="false"
>
<div class="stat-icon active">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<div class="stat-info">
<div class="stat-label">Active</div>
<div class="stat-value" id="stat-active">0</div>
</div>
</div>
<div
class="stat-card"
data-state="delayed"
role="tab"
aria-selected="false"
>
<div class="stat-icon delayed">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<polyline points="12 6 12 12 16 14" />
</svg>
</div>
<div class="stat-info">
<div class="stat-label">Delayed</div>
<div class="stat-value" id="stat-delayed">0</div>
</div>
</div>
<div
class="stat-card"
data-state="failed"
role="tab"
aria-selected="false"
>
<div class="stat-icon failed">
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
</div>
<div class="stat-info">
<div class="stat-label">Failed</div>
<div class="stat-value" id="stat-failed">0</div>
</div>
</div>
</div>
<div class="jobs-section">
<div class="jobs-header">
<p id="jobsContextLabel" class="jobs-context-label" aria-live="polite">
Showing Completed jobs
</p>
<div class="jobs-controls">
<select
id="pageSizeSelect"
class="page-size-select"
aria-label="Jobs per page"
title="Jobs per page"
>
<option value="10">10</option>
<option value="25" selected>25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
<div class="search-wrapper">
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<circle cx="11" cy="11" r="8" />
<path d="m21 21-4.35-4.35" />
</svg>
<input
type="text"
id="searchInput"
class="search-input"
placeholder="Search jobs..."
/>
</div>
</div>
</div>
<div
id="jobsBulkBar"
class="jobs-bulk-bar"
style="display: none"
role="region"
aria-label="Bulk actions for selected jobs"
>
<span id="bulkSelectionCount" class="bulk-selection-count"
>0 jobs selected</span
>
<div class="bulk-actions">
<button
type="button"
id="bulkClearBtn"
class="btn btn-secondary btn-sm"
>
Clear selection
</button>
<button
type="button"
id="bulkRetryBtn"
class="btn btn-success btn-sm"
style="display: none"
>
Retry selected
</button>
<button
type="button"
id="bulkDeleteBtn"
class="btn btn-danger btn-sm"
>
Delete selected
</button>
</div>
</div>
<div class="jobs-table-container">
<table class="jobs-table">
<thead>
<tr>
<th class="col-select" scope="col">
<input
type="checkbox"
id="selectAllJobsCheckbox"
title="Select all jobs in the current list (all pages; respects search filter)"
aria-label="Select all jobs in list"
/>
</th>
<th>ID</th>
<th>Name</th>
<th>State</th>
<th>Created</th>
<th>Attempts</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="jobs-table-body">
<tr>
<td colspan="7" class="loading-jobs">Loading jobs...</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination" id="pagination" style="display: none">
<button id="prevPage" class="btn btn-secondary btn-sm">
Previous
</button>
<span id="pageInfo" class="page-info">Page 1 of 1</span>
<button id="nextPage" class="btn btn-secondary btn-sm">
Next
</button>
</div>
</div>
</div>
<div id="job-detail-view" class="job-detail-view" style="display: none">
<div class="job-detail-toolbar">
<button
type="button"
id="jobDetailBackBtn"
class="btn btn-secondary btn-icon job-detail-back"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M19 12H5M12 19l-7-7 7-7" />
</svg>
Back to queue
</button>
<div class="job-detail-toolbar-live" aria-live="polite">
<span id="jobDetailUpdatedLabel" class="job-detail-updated-label"
>Updated —</span
>
<button
type="button"
id="jobDetailRefreshBtn"
class="icon-btn job-detail-toolbar-refresh"
title="Refresh job"
aria-label="Refresh job"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path
d="M23 4v6h-6M1 20v-6h6M3.51 9a9 9 0 0 1 14.85-3.48L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"
/>
</svg>
</button>
<label class="job-detail-auto-refresh">
<input type="checkbox" id="jobDetailAutoRefresh" checked />
<span>Auto</span>
</label>
</div>
<div class="job-detail-toolbar-actions">
<button
type="button"
id="retryJobBtn"
class="btn btn-success btn-icon"
style="display: none"
>
<svg
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M1 4v6h6M23 20v-6h-6" />
<path
d="M20.49 9A9 9 0 0 0 5.64 5.64L1 10m22 4l-4.64 4.36A9 9 0 0 1 3.51 15"
/>
</svg>
Retry job
</button>
<details class="job-detail-overflow" id="jobDetailOverflow">
<summary
class="job-detail-overflow-trigger icon-btn"
title="More actions"
aria-label="More actions"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<circle cx="12" cy="12" r="1" fill="currentColor" />
<circle cx="19" cy="12" r="1" fill="currentColor" />
<circle cx="5" cy="12" r="1" fill="currentColor" />
</svg>
</summary>
<div class="job-detail-overflow-menu" role="menu">
<button type="button" class="job-detail-overflow-item" id="jobDetailCopyLinkBtn" role="menuitem">
Copy link
</button>
<button type="button" class="job-detail-overflow-item" id="jobDetailCopyIdBtn" role="menuitem">
Copy job ID
</button>
<button type="button" class="job-detail-overflow-item job-detail-overflow-item--danger" id="deleteJobBtn" role="menuitem">
Delete job
</button>
</div>
</details>
</div>
</div>
<div class="job-detail-scroll">
<div id="jobDetailContent" class="job-detail-content-inner">
<p class="job-detail-loading">Loading job…</p>
</div>
</div>
</div>
</main>
</div>
<div
id="redisModalOverlay"
class="redis-modal-overlay"
hidden
role="dialog"
aria-modal="true"
aria-labelledby="redisModalTitle"
>
<div class="redis-modal" role="document">
<header class="redis-modal__head">
<h2 id="redisModalTitle" class="redis-modal__title">Redis</h2>
<button
type="button"
class="redis-modal__close icon-btn"
id="redisModalCloseBtn"
aria-label="Close"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
aria-hidden="true"
>
<path d="M18 6L6 18M6 6l12 12" />
</svg>
</button>
</header>
<p class="redis-modal__lede job-detail-muted">
Shared instance behind this dashboard (not per-queue job counts).
</p>
<div
id="queueRedisStatsBody"
class="redis-infra__body redis-modal__panel"
></div>
</div>
</div>
<script src="app.js?v=chainmq-ui-44"></script>
</body>
</html>