433 lines
22 KiB
HTML
433 lines
22 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin</title>
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/2.3.6/css/dataTables.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/3.0.7/css/responsive.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/buttons/3.0.2/css/buttons.dataTables.min.css">
|
|
<link href="https://cdn.datatables.net/columncontrol/1.2.0/css/columnControl.dataTables.min.css" rel="stylesheet">
|
|
|
|
<style>
|
|
body { background-color: #f8f9fa; }
|
|
.user-photo {
|
|
width: 45px;
|
|
height: 45px;
|
|
object-fit: cover;
|
|
border-radius: 10%;
|
|
border: 1px solid #ddd;
|
|
}
|
|
/* Style adjustments for DataTables 2.0 + Bootstrap */
|
|
.dt-container { margin-top: 20px; padding: 5px; }
|
|
/* Define the 9:16 vertical ratio */
|
|
/* Calculation: (16 / 9) * 100 = 177.77% */
|
|
.ratio-9x16 {
|
|
--bs-aspect-ratio: 177.7777777778%;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body>
|
|
<nav class="navbar navbar-dark bg-dark shadow-sm mb-4">
|
|
<div class="container">
|
|
<span class="navbar-brand">Admin Dashboard</span>
|
|
</div>
|
|
</nav>
|
|
|
|
<div class="container mb-5">
|
|
<div class="card shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3 mb-3">
|
|
<!-- Column 1: Total Records -->
|
|
<div class="col-md-3">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
Total Records:
|
|
<span id="total-records-count" class="badge bg-dark rounded-pill">0</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 2: Pending Submissions -->
|
|
<div class="col-md-3">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
Sonauto Pending:
|
|
<span id="pending-submissions" class="badge bg-warning rounded-pill">Loading...</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 3: Processing Submissions -->
|
|
<div class="col-md-3" style="display: none;">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
Video Queue:
|
|
<span id="processing-submissions" class="badge bg-info rounded-pill">Loading...</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Column 4: AI Credits -->
|
|
<div class="col-md-3">
|
|
<div class="alert alert-light mb-0 h-100 d-flex align-items-center">
|
|
<span>
|
|
SonautoAI Credits Remaining:
|
|
<span id="credits-remaining" class="badge bg-primary rounded-pill">Loading...</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="card shadow-sm dt-container">
|
|
<div class="card-dt">
|
|
<table id="usersTable" class="table table-striped w-100">
|
|
<thead>
|
|
<tr>
|
|
<th>Actions</th>
|
|
<th>Session</th>
|
|
<th>Created</th>
|
|
<th>Cookie ID</th>
|
|
<th>Owner Name</th>
|
|
<th>Pet Name</th>
|
|
<th>Photo</th>
|
|
<th>Pet Type</th>
|
|
<th>Vibe</th>
|
|
<th>Retries</th>
|
|
<th>Sent to LLM</th>
|
|
<th>Task ID</th>
|
|
<th>Recv from LLM</th>
|
|
<th>LLM Response</th>
|
|
<th>Full Response</th>
|
|
<th>Song Path</th>
|
|
<th>LLM Status</th>
|
|
<th>Lyrics</th>
|
|
<th>Vid Start</th>
|
|
<th>Vid End</th>
|
|
<th>Video Path</th>
|
|
<th>Entry Status</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody></tbody> <!-- Empty: DataTables will fill this -->
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 1. Use modal-xl for desktop real estate -->
|
|
<div class="modal fade" id="photoModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Record Details</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="row">
|
|
<!-- Left Column: Media (Fixed Width for 9:16) -->
|
|
<div class="col-md-4 border-end">
|
|
<h6>Original Photo</h6>
|
|
<img src="" id="modalImage" class="img-thumbnail mb-3"
|
|
style="width: 150px; height: 150px; object-fit: cover;" loading="lazy">
|
|
|
|
<h6 class="mt-3">Generated Assets</h6>
|
|
<div id="modalMediaContainer" class="d-flex flex-column gap-3">
|
|
<!-- Video and Audio injected here -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Right Column: Full Data Table -->
|
|
<div class="col-md-8">
|
|
<h6>Full Entry Metadata</h6>
|
|
<div class="table-responsive">
|
|
<table class="table table-sm table-hover border">
|
|
<thead class="table-light">
|
|
<tr>
|
|
<th>Field</th>
|
|
<th>Value</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modalDetailsTableBody">
|
|
<!-- Dynamic content -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<h6 class="mt-4">Lyrics</h6>
|
|
|
|
<pre id="modalLyrics" class="bg-dark text-info p-3 rounded"
|
|
style="white-space: pre-wrap; font-size: 0.85rem; max-height: 300px; overflow-y: auto;">
|
|
</pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script defer src="./assets/vendor/alpine.min.js"></script>
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.datatables.net/2.3.6/js/dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/responsive/3.0.7/js/dataTables.responsive.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/3.0.2/js/dataTables.buttons.min.js"></script>
|
|
<script src="https://cdn.datatables.net/buttons/3.0.2/js/buttons.colVis.min.js"></script>
|
|
<script src="https://cdn.datatables.net/columncontrol/1.2.0/js/dataTables.columnControl.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/luxon@3.7.2/build/global/luxon.min.js"></script>
|
|
|
|
<script>
|
|
$(document).ready(function () {
|
|
const API_BASE_URL = '';
|
|
const API_ENDPOINT = `/back/api/admin/data`;
|
|
const MEDIA_BASE_URL = 'https://storage.googleapis.com/vday2026/';
|
|
|
|
// Initialize the Modal object once so we can reuse it
|
|
const detailModalElement = document.getElementById('photoModal');
|
|
const detailModal = new bootstrap.Modal(detailModalElement);
|
|
|
|
// Helper to convert container paths to web URLs
|
|
function fixStoragePath(path) {
|
|
if (!path) return '';
|
|
// let relative_path = path.replace(/^\/app\/storage\//, '/storage/');
|
|
const isRelative = path.startsWith('uploads/') || path.startsWith('audio/') || path.startsWith('video/');
|
|
return isRelative ? MEDIA_BASE_URL + path : path;
|
|
}
|
|
|
|
// Fetch and update system status metrics
|
|
function loadCredits() {
|
|
const STATUS_ENDPOINT = `/back/api/admin/queue-status`;
|
|
fetch(STATUS_ENDPOINT)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Update all three metrics from the API
|
|
$('#credits-remaining').text((data.sonauto_credits || 0).toLocaleString());
|
|
$('#pending-submissions').text((data.pending_submissions || 0).toLocaleString());
|
|
$('#processing-submissions').text((data.processing_submissions || 0).toLocaleString());
|
|
})
|
|
.catch(err => {
|
|
console.error('Failed to load credits:', err);
|
|
$('#credits-remaining').text('Error');
|
|
$('#pending-submissions').text('Error');
|
|
$('#processing-submissions').text('Error');
|
|
});
|
|
}
|
|
loadCredits();
|
|
// Poll every 2 minutes
|
|
setInterval(loadCredits, (2*60*1000));
|
|
|
|
// Initialize DataTable
|
|
const table = $('#usersTable').DataTable({
|
|
serverSide: true,
|
|
processing: true,
|
|
pageLength: 50,
|
|
order: {
|
|
name: 'created_at',
|
|
dir: 'desc'
|
|
},
|
|
deferRender: true,
|
|
responsive: {
|
|
details: {
|
|
type: 'column',
|
|
target: 'tr' // Clicking the row expands it
|
|
}
|
|
},
|
|
// Define which columns are visible by default
|
|
columnDefs: [
|
|
{ responsivePriority: 1, targets: [3, 4, 12, 5] }, // Owner, Pet, Status, Photo
|
|
{ responsivePriority: 2, targets: [1, 20] }, // CreatedAt, EntryStatus
|
|
{ targets: [2, 6, 8, 9, 10, 11, 14, 17, 18], visible: false } // Hide technical IDs by default
|
|
],
|
|
ajax: {
|
|
url: API_ENDPOINT,
|
|
dataSrc: function (json) {
|
|
// Your FastAPI endpoint returns AdminResponse directly
|
|
// Structure: { draw, recordsTotal, recordsFiltered, data }
|
|
|
|
// console.log('DEBUG: API Response:', json);
|
|
|
|
// Update the total records badge at the top
|
|
$('#total-records-count').text((json.recordsTotal || 0).toLocaleString());
|
|
|
|
// DataTables expects these fields at root level (already there!)
|
|
// No need to move anything - just return the data array
|
|
return json.data || [];
|
|
}
|
|
},
|
|
layout: {
|
|
topStart: {
|
|
buttons: [
|
|
{
|
|
extend: 'colvis',
|
|
text: 'Select Columns',
|
|
className: 'btn btn-sm btn-outline-secondary'
|
|
}
|
|
]
|
|
},
|
|
topEnd: 'search',
|
|
bottomStart: 'info',
|
|
bottomEnd: 'paging'
|
|
},
|
|
columns: [
|
|
{
|
|
data: null,
|
|
title: 'Actions',
|
|
name: 'actions',
|
|
defaultContent: '',
|
|
orderable: false,
|
|
searchable: false,
|
|
render: function (data, type, row) {
|
|
return `<button class="btn btn-sm btn-primary view-details-btn" type="button">
|
|
View
|
|
</button>`;
|
|
}
|
|
},
|
|
{ data: 'session_id', title: 'Session', name: 'session_id', defaultContent: 'N/A', visible: false },
|
|
{
|
|
data: 'created_at',
|
|
title: 'Created',
|
|
ordering: true,
|
|
name: 'created_at',
|
|
defaultContent: 'N/A',
|
|
visible: true,
|
|
render: (data) => {
|
|
if (!data) return 'N/A';
|
|
return luxon.DateTime.fromISO(data).toFormat('yyyy-MM-dd HH:mm:ss') + ' UTC';
|
|
}
|
|
},
|
|
{ data: 'cookie_id', title: 'Cookie ID', name: 'cookie_id', defaultContent: 'N/A', visible: false },
|
|
{ data: 'owner_name', title: 'Owner', name: 'owner_name', defaultContent: 'N/A' },
|
|
{ data: 'pet_name', title: 'Pet Name', name: 'pet_name', defaultContent: 'N/A' },
|
|
{
|
|
data: 'photo_path',
|
|
title: 'Photo',
|
|
name: 'photo_path',
|
|
defaultContent: '',
|
|
visible: true,
|
|
render: (data) => {
|
|
const imgPath = fixStoragePath(data) || "https://placehold.co/500x500/fbba0e/e11d48/jpg?text=No\nImage";
|
|
return `<img src="${imgPath}" class="user-photo img-preview" style="cursor:pointer">`;
|
|
}
|
|
},
|
|
{ data: 'pet_type', name: 'pet_type', title: 'Type', defaultContent: 'N/A' },
|
|
{ data: 'music_vibe', name: 'music_vibe', title: 'Vibe', defaultContent: 'N/A' },
|
|
{ data: 'retry_count', name: 'retry_count', title: 'Retries', defaultContent: 'N/A', visible: false },
|
|
{ data: 'sent_to_LLM', name: 'sent_to_LLM', title: 'Sent to LLM', defaultContent: 'N/A' },
|
|
{ data: 'LLM_task_id', name: 'LLM_task_id', title: 'Task ID', defaultContent: 'N/A', visible: false },
|
|
{ data: 'received_from_LLM', name: 'received_from_LLM', title: 'LLM Recv', defaultContent: 'N/A', visible: false },
|
|
{
|
|
data: 'LLM_response',
|
|
title: 'LLM Response',
|
|
name: 'LLM_response',
|
|
defaultContent: 'N/A',
|
|
visible: false,
|
|
render: (data) => data ? (data.substring(0, 30) + '...') : 'Pending'
|
|
},
|
|
{ data: 'LLM_full_response', name: 'LLM_full_response', defaultContent: 'N/A', visible: false },
|
|
{ data: 'generated_song_path', name: 'generated_song_path', title: 'Song', defaultContent: '', render: d => d ? 'Yes' : 'No' },
|
|
{ data: 'LLM_status', name: 'LLM_status', title: 'LLM Status', defaultContent: 'N/A', visible: true },
|
|
{ data: 'lyrics', name: 'lyrics', title: 'Lyrics', defaultContent: '', visible: false },
|
|
{ data: 'video_creation_start', name: 'video_creation_start', title: 'Vid Start', defaultContent: '', visible: false },
|
|
{ data: 'video_creation_end', name: 'video_creation_end', title: 'Vid End', defaultContent: '', visible: false },
|
|
{ data: 'generated_video_path', name: 'generated_video_path', title: 'VideoGen', defaultContent: '', render: d => d ? 'Yes' : 'No', visible: false },
|
|
{
|
|
data: 'entry_status',
|
|
title: 'Status',
|
|
name: 'entry_status',
|
|
defaultContent: '',
|
|
visible: true ,
|
|
render: (data) => {
|
|
return `<span>${data}</span>`;
|
|
}
|
|
}
|
|
]
|
|
});
|
|
|
|
// Use event delegation (so it works even after searching/paging)
|
|
$('#usersTable').on('click', '.view-details-btn, .img-preview', function () {
|
|
// 1. Get the data for the specific row clicked
|
|
// We use .closest('tr') to find the table row containing the clicked image
|
|
const rowData = table.row($(this).closest('tr')).data();
|
|
|
|
// 2. Populate the Modal fields
|
|
$('.modal-title').text(`Session: ${rowData.session_id}`);
|
|
$('#modalImage').attr('src', fixStoragePath(rowData.photo_path) || "https://placehold.co/500x500/fbba0e/e11d48/jpg?text=No\nImage");
|
|
|
|
// 3. Dynamic Table Generation for all 20+ columns
|
|
const tableBody = $('#modalDetailsTableBody');
|
|
tableBody.empty();
|
|
// List of keys we want to display or skip
|
|
Object.entries(rowData).forEach(([key, value]) => {
|
|
// Skip keys that are handled visually (media/lyrics)
|
|
if (['photo_path', 'generated_video_path', 'generated_song_path', 'lyrics'].includes(key))
|
|
return;
|
|
|
|
let displayValue = value ?? '<span class="text-muted italic">null</span>';
|
|
// Surgical Link Detection: Only link if it has a known file extension
|
|
const isFile = typeof value === 'string' && /\.(jpg|jpeg|png|mp3|mp4|wav)$/i.test(value);
|
|
if (isFile) {
|
|
const fullUrl = fixStoragePath(value);
|
|
displayValue = `<a href="${fullUrl}" target="_blank" class="text-decoration-none text-truncate d-inline-block" style="max-width: 300px;">${value} 🔗</a>`;
|
|
}
|
|
tableBody.append(`
|
|
<tr>
|
|
<td class="fw-bold text-muted" style="width: 30%">${key.replace(/_/g, ' ')}</td>
|
|
<td class="text-break">${displayValue}</td>
|
|
</tr>
|
|
`);
|
|
});
|
|
|
|
$('#modalLyrics').text(rowData.lyrics || 'No lyrics generated.');
|
|
|
|
|
|
// 4. Handle Media (Song or Video)
|
|
const mediaContainer = $('#modalMediaContainer');
|
|
// Define the URLs before using them
|
|
const audioUrl = fixStoragePath(rowData.generated_song_path);
|
|
const videoUrl = fixStoragePath(rowData.generated_video_path);
|
|
|
|
// 1. Clear previous content
|
|
mediaContainer.empty();
|
|
|
|
// 2. Check for Audio
|
|
if (rowData.generated_song_path) {
|
|
console.log('Adding Audio Player...');
|
|
mediaContainer.append(`
|
|
<div class="audio-section">
|
|
<label class="small text-muted mb-1">Audio Track:</label>
|
|
<code class="d-block mb-2 text-break p-2 bg-light border rounded" style="font-size: 0.7rem;">${audioUrl}</code>
|
|
<audio controls preload="metadata" class="w-100" src="${audioUrl}"></audio>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// 3. Check for Video
|
|
if (rowData.generated_video_path) {
|
|
console.log('Adding Video Player...');
|
|
mediaContainer.append(`
|
|
<div class="video-section mb-3">
|
|
<label class="small text-muted mb-1">Video (9:16):</label>
|
|
<code class="d-block mb-2 text-break p-2 bg-light border rounded" style="font-size: 0.7rem;">${videoUrl}</code>
|
|
<div class="ratio ratio-9x16 bg-black rounded shadow-sm overflow-hidden mx-auto" style="max-width: 220px;">
|
|
<video controls preload="metadata" class="w-100 h-100" src="${videoUrl}">
|
|
Your browser does not support the video tag.
|
|
</video>
|
|
</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
if (!rowData.generated_song_path && !rowData.generated_video_path) {
|
|
mediaContainer.html('<div class="d-flex align-items-center justify-content-center h-100 text-white-50">No Media</div>');
|
|
}
|
|
|
|
// 5. Show the Modal
|
|
detailModal.show();
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|