simple-queue-web 0.1.0

Web UI for inspecting and managing simple-queue persistent job queues backed by PostgreSQL
<!DOCTYPE html>
<html lang="en" class="h-full">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{% block title %}Queue Manager{% endblock %}</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <script>
        tailwind.config = {
            theme: {
                extend: {}
            }
        }
    </script>
    <script type="module">
        import { Datastar } from 'https://cdn.jsdelivr.net/npm/@starfederation/datastar/+esm';
    </script>
</head>
<body class="h-full bg-gray-950 text-gray-100 antialiased">
    <nav class="bg-gray-900 border-b border-gray-800 px-6 py-3 sticky top-0 z-50">
        <div class="max-w-7xl mx-auto flex items-center justify-between">
            <div class="flex items-center gap-6">
                <a href="/dashboard" class="text-lg font-bold text-white tracking-tight">Queue Manager</a>
                <a href="/dashboard" class="text-sm text-gray-400 hover:text-white transition-colors">Dashboard</a>
                <a href="/queues/browse" class="text-sm text-gray-400 hover:text-white transition-colors">Queues</a>
            </div>
            <div class="flex items-center gap-4">
                <form id="job-search" class="flex items-center gap-1" onsubmit="document.getElementById('job-search-input').blur()">
                    <input type="text" id="job-search-input" placeholder="Job ID"
                           class="bg-gray-800 border border-gray-700 text-white text-xs rounded px-2 py-1 w-36 focus:outline-none focus:ring-1 focus:ring-blue-500 placeholder-gray-500">
                </form>
                <label class="flex items-center gap-2 text-sm text-gray-400">
                    Poll:
                    <select id="poll-interval"
                            class="bg-gray-800 border border-gray-700 text-white text-sm rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-500">
                        <option value="200">0.2s</option>
                        <option value="500">0.5s</option>
                        <option value="1000">1s</option>
                        <option value="2000" selected>2s</option>
                        <option value="5000">5s</option>
                        <option value="10000">10s</option>
                        <option value="30000">30s</option>
                        <option value="0">Off</option>
                    </select>
                </label>
                <span id="connection-status" class="text-xs text-gray-600">--</span>
            </div>
        </div>
    </nav>
    <main class="max-w-7xl mx-auto px-4 sm:px-6 py-6">
        {% block content %}{% endblock %}
    </main>
    <script>
        let pollMs = 2000;
        let pollTimer = null;

        function processResponse(text) {
            const blocks = text.trim().split('\n\n');
            for (const block of blocks) {
                const lines = block.split('\n');
                const directive = lines[0].trim();
                if (!directive.startsWith('selector ')) continue;
                const selector = directive.slice(9).trim();
                const html = lines.slice(1).join('\n');
                const el = document.querySelector(selector);
                if (el) {
                    el.innerHTML = html.trim();
                }
            }
        }

        function getPollEndpoint() {
            const path = window.location.pathname;
            const params = new URLSearchParams(window.location.search);
            if (path === '/dashboard') return '/api/dashboard/poll';
            if (path === '/queues/browse') {
                let ep = '/api/queues/poll?';
                const keep = ['queue', 'status', 'source', 'page', 'sort_by', 'sort_dir'];
                const filtered = new URLSearchParams();
                for (const k of keep) {
                    if (params.has(k)) filtered.set(k, params.get(k));
                }
                return '/api/queues/poll?' + filtered.toString();
            }
            return null;
        }

        function poll() {
            const endpoint = getPollEndpoint();
            if (!endpoint) return;
            const status = document.getElementById('connection-status');
            fetch(endpoint)
                .then(r => {
                    if (r.ok) {
                        status.textContent = 'Live';
                        status.className = 'text-xs text-green-500';
                        return r.text();
                    }
                    throw new Error(r.status);
                })
                .then(text => processResponse(text))
                .catch(err => {
                    status.textContent = 'Error';
                    status.className = 'text-xs text-red-500';
                    console.error('Poll error:', err);
                });
        }

        function startPolling() {
            stopPolling();
            if (pollMs > 0) {
                poll();
                pollTimer = setInterval(poll, pollMs);
            } else {
                const status = document.getElementById('connection-status');
                status.textContent = 'Paused';
                status.className = 'text-xs text-yellow-500';
            }
        }

        function stopPolling() {
            if (pollTimer) {
                clearInterval(pollTimer);
                pollTimer = null;
            }
        }

        document.getElementById('poll-interval').addEventListener('change', function() {
            pollMs = parseInt(this.value);
            startPolling();
        });

        document.getElementById('job-search').addEventListener('submit', function(e) {
            e.preventDefault();
            var id = document.getElementById('job-search-input').value.trim();
            if (id) window.location.href = '/jobs/' + id + '?source=auto';
        });

        startPolling();
    </script>
</body>
</html>