simple-queue-web 0.1.0

Web UI for inspecting and managing simple-queue persistent job queues backed by PostgreSQL
{% extends "base.html" %}

{% block title %}Queues - Queue Manager{% endblock %}

{% block content %}
<div class="space-y-6">
    <div class="flex items-center justify-between">
        <h1 class="text-2xl font-bold text-white">Queue Browser</h1>
        <div class="flex items-center gap-3">
            <select name="source" id="source-select"
                    class="bg-gray-800 border border-gray-700 text-white text-sm rounded px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
                    onchange="navigateWithSource(this.value)">
                <option value="queue" {% if t.is_queue_source %}selected{% endif %}>Queue</option>
                <option value="dlq" {% if t.is_dlq_source %}selected{% endif %}>DLQ</option>
                <option value="archive" {% if t.is_archive_source %}selected{% endif %}>Archive</option>
            </select>
            <form method="get" id="filter-form" class="flex items-center gap-3">
                <input type="hidden" name="source" value="{{ t.selected_source }}">
                <select name="queue"
                        class="bg-gray-800 border border-gray-700 text-white text-sm rounded px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
                        onchange="this.form.submit()">
                    <option value="">All Queues</option>
                    {% for (q, sel) in queues %}
                    <option value="{{ q }}" {% if sel %}selected{% endif %}>{{ q }}</option>
                    {% endfor %}
                </select>
                <select name="status"
                        class="bg-gray-800 border border-gray-700 text-white text-sm rounded px-3 py-1.5 focus:outline-none focus:ring-1 focus:ring-blue-500"
                        onchange="this.form.submit()">
                    <option value="">All Statuses</option>
                    <option value="pending" {% if t.sel_pending %}selected{% endif %}>Pending</option>
                    <option value="running" {% if t.sel_running %}selected{% endif %}>Running</option>
                    <option value="completed" {% if t.sel_completed %}selected{% endif %}>Completed</option>
                    <option value="failed" {% if t.sel_failed %}selected{% endif %}>Failed</option>
                </select>
            </form>
        </div>
    </div>

    <div id="job-table-wrapper">
        <div class="bg-gray-900 rounded-lg border border-gray-800 overflow-hidden">
            {% if t.jobs.is_empty() %}
            <div class="px-4 py-12 text-center text-gray-500">No jobs found</div>
            {% else %}
            <div class="overflow-x-auto">
                <table class="w-full text-sm">
                    <thead>
                        <tr class="text-left text-gray-400 text-xs uppercase tracking-wider">
                            <th class="px-4 py-3">ID</th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_status }}" class="hover:text-white">Status</a></th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_attempt }}" class="hover:text-white">Atmpt</a></th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_reprocess_count }}" class="hover:text-white">Reproc</a></th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_created_at }}" class="hover:text-white">Created At</a></th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_run_at }}" class="hover:text-white">Run At</a></th>
                            <th class="px-4 py-3"><a href="{{ t.sort_link_updated_at }}" class="hover:text-white">Updated At</a></th>
                            <th class="px-4 py-3">Actions</th>
                        </tr>
                    </thead>
                    <tbody>
                        {% for job in t.jobs %}
                        <tr class="border-t border-gray-800 hover:bg-gray-800/50">
                            <td class="px-4 py-2">
                                <a href="/jobs/{{ job.id }}?source={{ job.source }}"
                                   class="text-blue-400 hover:text-blue-300 font-mono text-xs">
                                    {{ job.short_id }}
                                </a>
                            </td>
                            <td class="px-4 py-2">
                                <span class="text-xs px-2 py-0.5 rounded
                                    {% if job.status == "pending" %}bg-yellow-900/50 text-yellow-300
                                    {% else if job.status == "running" %}bg-blue-900/50 text-blue-300
                                    {% else if job.status == "completed" %}bg-green-900/50 text-green-300
                                    {% else if job.status == "failed" %}bg-red-900/50 text-red-300
                                    {% else if job.status == "cancelled" %}bg-gray-800 text-gray-500
                                    {% else %}bg-gray-800 text-gray-400{% endif %}">
                                    {{ job.status }}
                                </span>
                            </td>
                            <td class="px-4 py-2 text-gray-300 font-mono text-xs">{{ job.attempt }}/{{ job.max_attempts }}</td>
                            <td class="px-4 py-2 text-gray-400 font-mono text-xs">{{ job.reprocess_count }}</td>
                            <td class="px-4 py-2 text-xs"><div class="leading-none">{{ job.created_at_date }}</div><div class="text-[10px] text-gray-500 leading-none mt-0.5">{{ job.created_at_time }}</div></td>
                            <td class="px-4 py-2 text-xs"><div class="leading-none">{{ job.run_at_date }}</div><div class="text-[10px] text-gray-500 leading-none mt-0.5">{{ job.run_at_time }}</div></td>
                            <td class="px-4 py-2 text-xs"><div class="leading-none">{{ job.updated_at_date }}</div><div class="text-[10px] text-gray-500 leading-none mt-0.5">{{ job.updated_at_time }}</div></td>
                            <td class="px-4 py-2">
                                <div class="flex items-center gap-1">
                                    <a href="/jobs/{{ job.id }}?source={{ job.source }}"
                                       class="text-xs text-blue-400 hover:text-blue-300 px-2 py-1">Inspect</a>
                                    {% if t.is_queue_source %}
                                    <form method="post" action="/jobs/{{ job.id }}/restart?queue={{ t.selected_queue }}&page={{ t.page }}&source=queue"
                                          class="inline" onsubmit="return confirm('Restart this job?')">
                                        <button type="submit" class="text-xs text-yellow-400 hover:text-yellow-300 px-2 py-1">Restart</button>
                                    </form>
                                    <form method="post" action="/jobs/{{ job.id }}/cancel?queue={{ t.selected_queue }}&page={{ t.page }}&source=queue"
                                          class="inline" onsubmit="return confirm('Cancel this job?')">
                                        <button type="submit" class="text-xs text-red-400 hover:text-red-300 px-2 py-1">Cancel</button>
                                    </form>
                                    {% endif %}
                                    {% if t.is_dlq_source || t.is_archive_source %}
                                    <form method="post" action="/jobs/{{ job.id }}/requeue?queue={{ t.selected_queue }}&page={{ t.page }}&source={{ t.selected_source }}"
                                          class="inline" onsubmit="return confirm('Move this job to queue as pending?')">
                                        <button type="submit" class="text-xs text-green-400 hover:text-green-300 px-2 py-1">Requeue</button>
                                    </form>
                                    {% endif %}
                                </div>
                            </td>
                        </tr>
                        {% endfor %}
                    </tbody>
                </table>
            </div>
            <div class="px-4 py-3 border-t border-gray-800 flex items-center justify-between text-sm">
                <div class="text-gray-400">
                    {{ t.show_from }}-{{ t.show_to }} of {{ t.total }}
                </div>
                <div class="flex items-center gap-2">
                    {% if t.page > 1 %}
                    <a href="/queues/browse?queue={{ t.selected_queue }}&source={{ t.selected_source }}&sort_by={{ t.sort_by }}&sort_dir={{ t.sort_dir }}&page={{ t.page - 1 }}"
                       class="px-3 py-1 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded text-xs transition-colors">
                        Prev
                    </a>
                    {% endif %}
                    <span class="text-gray-500 text-xs">Page {{ t.page }} of {{ t.total_pages }}</span>
                    {% if t.page < t.total_pages %}
                    <a href="/queues/browse?queue={{ t.selected_queue }}&source={{ t.selected_source }}&sort_by={{ t.sort_by }}&sort_dir={{ t.sort_dir }}&page={{ t.page + 1 }}"
                       class="px-3 py-1 bg-gray-800 hover:bg-gray-700 text-gray-300 rounded text-xs transition-colors">
                        Next
                    </a>
                    {% endif %}
                </div>
            </div>
            {% endif %}
        </div>
    </div>
</div>

<script>
function navigateWithSource(source) {
    const params = new URLSearchParams(window.location.search);
    params.set('source', source);
    window.location.search = params.toString();
}
</script>
{% endblock %}