Files
furumusic/templates/admin/reviews.html
T
ab dcc665563a
Build and Publish / Build and Publish Docker Image (push) Successful in 2m47s
Reworked Reviews page
2026-05-25 13:50:24 +03:00

277 lines
12 KiB
HTML

{% extends "admin/layout.html" %}
{% block admin_title %}{{ t.nav_reviews }}{% endblock admin_title %}
{% block content %}
<h1>{{ t.reviews_heading }}</h1>
<div style="margin-bottom: 1rem; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap;">
<a href="/admin/reviews" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "" %} #333; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_all }}</a>
<a href="/admin/reviews?status=pending" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "pending" %} #ffc107; color: #000{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_pending }}</a>
<a href="/admin/reviews?status=approved" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "approved" %} #28a745; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_approved }}</a>
<a href="/admin/reviews?status=rejected" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "rejected" %} #dc3545; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_rejected }}</a>
<a href="/admin/reviews?status=queued" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "queued" %} #17a2b8; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_queued }}</a>
<a href="/admin/reviews?status=processing" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "processing" %} #007bff; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_processing }}</a>
<a href="/admin/reviews?status=auto_approved" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "auto_approved" %} #28a745; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_auto_approved }}</a>
<a href="/admin/reviews?status=failed" style="padding: .3rem .8rem; border-radius: 4px; text-decoration: none; background:{% if status_filter == "failed" %} #dc3545; color: #fff{% else %} #e9ecef; color: #333{% endif %};">{{ t.reviews_filter_failed }}</a>
{% if !rows.is_empty() %}
<span style="flex:1;"></span>
<form method="post" action="/admin/reviews/clear{% if !status_filter.is_empty() %}?status={{ status_filter }}{% endif %}" style="margin:0;" onsubmit="return confirm('{{ t.reviews_clear_confirm }}');">
<button type="submit" style="padding:.3rem .8rem; border-radius:4px; border:1px solid #dc3545; background:#fff; color:#dc3545; cursor:pointer;">{% if status_filter.is_empty() %}{{ t.reviews_clear_all }}{% else %}{{ t.reviews_clear_filtered }}{% endif %}</button>
</form>
{% endif %}
</div>
{% if rows.is_empty() %}
<p>{{ t.reviews_empty }}</p>
{% else %}
<form id="reviews-bulk-form" method="post" action="/admin/reviews/bulk" style="margin:0;">
<input type="hidden" name="selected_ids" id="selected-review-ids" value="">
<input type="hidden" name="status_filter" value="{{ status_filter }}">
<div class="review-bulk-toolbar">
<button type="button" id="select-shown-reviews" class="review-toolbar-button">{{ t.reviews_select_all }}</button>
<button type="button" id="clear-review-selection" class="review-toolbar-button">{{ t.reviews_clear_selection }}</button>
<button type="submit" name="action" value="delete" class="review-danger-button" disabled>{{ t.reviews_delete_selected }}</button>
<button type="submit" name="action" value="requeue" class="review-primary-button" disabled>{{ t.reviews_requeue_selected }}</button>
<span id="review-selection-summary" class="review-selection-summary">{{ t.reviews_selected_none }}</span>
</div>
<table>
<tr>
<th class="review-select-cell"></th>
<th>ID</th>
<th>{{ t.reviews_status }}</th>
<th>{{ t.reviews_type }}</th>
<th>{{ t.reviews_input_path }}</th>
<th>{{ t.reviews_tags }}</th>
<th>{{ t.reviews_confidence }}</th>
<th>{{ t.reviews_model }}</th>
<th>{{ t.reviews_llm_duration }}</th>
<th>{{ t.reviews_tokens }}</th>
<th>{{ t.reviews_created }}</th>
<th>{{ t.jobs_actions }}</th>
</tr>
{% for row in rows %}
<tr>
<td class="review-select-cell">
<input type="checkbox" class="review-select" value="{{ row.review.id_val() }}" data-status="{{ row.review.status_str() }}" aria-label="Select review {{ row.review.id_val() }}">
</td>
<td><a href="/admin/reviews/{{ row.review.id_val() }}">{{ row.review.id_val() }}</a></td>
<td><span class="badge {{ row.review.status_badge_class() }}">{{ row.review.status_str() }}</span></td>
<td>{{ row.review.review_type_str() }}</td>
<td class="review-input-path" title="{{ row.review.input_path_str() }}">{{ row.display_input_path }}</td>
<td class="review-tag-cell">
{% for tag in row.media_tags %}
<span class="review-tag review-tag-{{ tag.kind }}">{{ tag.label }}</span>
{% endfor %}
</td>
<td>{% match row.review.confidence() %}{% when Some with (c) %}{{ c }}{% when None %}-{% endmatch %}</td>
{% match stats_map.get(&row.review.id_val()) %}
{% when Some with (s) %}
<td>{{ s.model_name }}</td>
<td>{{ s.duration_display() }}</td>
<td>{{ s.tokens_display() }}</td>
{% when None %}
<td>-</td>
<td>-</td>
<td>-</td>
{% endmatch %}
<td>{{ row.review.created_at_str() }}</td>
<td>
<a href="/admin/reviews/{{ row.review.id_val() }}">{{ t.reviews_view }}</a>
</td>
</tr>
{% endfor %}
</table>
</form>
{% endif %}
<style>
.review-bulk-toolbar {
margin-bottom: .75rem;
display: flex;
gap: .5rem;
align-items: center;
flex-wrap: wrap;
}
.review-toolbar-button,
.review-danger-button,
.review-primary-button {
padding: .35rem .7rem;
border-radius: 4px;
border: 1px solid #ced4da;
background: #fff;
color: #212529;
cursor: pointer;
}
.review-danger-button {
border-color: #dc3545;
color: #dc3545;
}
.review-primary-button {
border-color: #17a2b8;
color: #0c5460;
}
.review-danger-button:disabled,
.review-primary-button:disabled {
cursor: not-allowed;
opacity: .45;
}
.review-selection-summary {
min-height: 1.7rem;
padding: .35rem .6rem;
border: 1px solid #dee2e6;
border-radius: 4px;
background: #f8f9fa;
color: #495057;
font-size: .9rem;
white-space: nowrap;
}
.review-select-cell {
width: 2.25rem;
text-align: center;
}
.review-select {
width: 1rem;
height: 1rem;
}
.review-input-path {
max-width: 34rem;
white-space: normal;
overflow-wrap: anywhere;
}
.review-tag-cell {
max-width: 18rem;
}
.review-tag {
display: inline-block;
margin: .1rem .15rem .1rem 0;
padding: .12rem .35rem;
border: 1px solid #ced4da;
border-radius: 4px;
background: #f8f9fa;
color: #495057;
font-size: .8rem;
white-space: nowrap;
}
.review-tag-format { border-color: #9ec5fe; background: #e7f1ff; color: #084298; }
.review-tag-bitrate { border-color: #a3cfbb; background: #d1e7dd; color: #0f5132; }
.review-tag-sample { border-color: #ffda6a; background: #fff3cd; color: #664d03; }
.review-tag-depth { border-color: #d0bfff; background: #f0e7ff; color: #3d246c; }
.review-tag-size { border-color: #ced4da; background: #f8f9fa; color: #495057; }
.badge-completed { background: #d4edda; color: #155724; }
.badge-failed { background: #f8d7da; color: #721c24; }
.badge-pending { background: #fff3cd; color: #856404; }
.badge-queued { background: #d1ecf1; color: #0c5460; }
.badge-processing { background: #cce5ff; color: #004085; }
</style>
<script>
(() => {
const form = document.getElementById("reviews-bulk-form");
if (!form) {
return;
}
const checkboxes = Array.from(form.querySelectorAll(".review-select"));
const selectedIdsInput = document.getElementById("selected-review-ids");
const summary = document.getElementById("review-selection-summary");
const selectShownButton = document.getElementById("select-shown-reviews");
const clearSelectionButton = document.getElementById("clear-review-selection");
const submitButtons = Array.from(form.querySelectorAll("button[type='submit']"));
const selected = new Set();
const statusCounts = new Map();
const labels = {
pending: "{{ t.reviews_filter_pending }}",
approved: "{{ t.reviews_filter_approved }}",
rejected: "{{ t.reviews_filter_rejected }}",
queued: "{{ t.reviews_filter_queued }}",
processing: "{{ t.reviews_filter_processing }}",
auto_approved: "{{ t.reviews_filter_auto_approved }}",
failed: "{{ t.reviews_filter_failed }}"
};
function setStatusCount(status, delta) {
const next = (statusCounts.get(status) || 0) + delta;
if (next > 0) {
statusCounts.set(status, next);
} else {
statusCounts.delete(status);
}
}
function syncControls() {
selectedIdsInput.value = Array.from(selected).join(",");
const total = selected.size;
for (const button of submitButtons) {
button.disabled = total === 0;
}
if (total === 0) {
summary.textContent = "{{ t.reviews_selected_none }}";
return;
}
const parts = Array.from(statusCounts.entries())
.sort(([a], [b]) => a.localeCompare(b))
.map(([status, count]) => `${labels[status] || status}: ${count}`);
summary.textContent = `{{ t.reviews_selected_prefix }}: ${total} (${parts.join(", ")})`;
}
function setChecked(checkbox, checked) {
const id = checkbox.value;
const isSelected = selected.has(id);
checkbox.checked = checked;
if (isSelected === checked) {
return;
}
const status = checkbox.dataset.status || "unknown";
if (checked) {
selected.add(id);
setStatusCount(status, 1);
} else {
selected.delete(id);
setStatusCount(status, -1);
}
}
for (const checkbox of checkboxes) {
checkbox.addEventListener("change", () => {
setChecked(checkbox, checkbox.checked);
syncControls();
});
}
selectShownButton.addEventListener("click", () => {
for (const checkbox of checkboxes) {
setChecked(checkbox, true);
}
syncControls();
});
clearSelectionButton.addEventListener("click", () => {
for (const checkbox of checkboxes) {
setChecked(checkbox, false);
}
syncControls();
});
form.addEventListener("submit", (event) => {
syncControls();
if (selected.size === 0) {
event.preventDefault();
alert("{{ t.reviews_none_selected_confirm }}");
return;
}
const action = event.submitter ? event.submitter.value : "";
const message = action === "requeue"
? "{{ t.reviews_requeue_selected_confirm }}"
: "{{ t.reviews_delete_selected_confirm }}";
if (!confirm(message)) {
event.preventDefault();
}
});
syncControls();
})();
</script>
{% endblock content %}