dev #62
@@ -887,3 +887,193 @@ body {
|
||||
.d-flex { display: flex; }
|
||||
.justify-content-between { justify-content: space-between; }
|
||||
.align-items-center { align-items: center; }
|
||||
|
||||
/* Database Admin Tools Styles */
|
||||
.query-results {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.query-info {
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.query-info.success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.query-info.error {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.results-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.875rem;
|
||||
background: white;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.results-table th {
|
||||
background-color: var(--dark-color);
|
||||
color: white;
|
||||
padding: 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.results-table td {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.results-table tr:hover {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
.results-table td em {
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.admin-tools {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-tools .btn {
|
||||
text-align: left;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.admin-results {
|
||||
margin-top: 1rem;
|
||||
padding: 1rem;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.scan-summary,
|
||||
.fix-summary {
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.scan-summary {
|
||||
background-color: #e7f3ff;
|
||||
border: 1px solid #b3d9ff;
|
||||
}
|
||||
|
||||
.fix-summary.success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.fix-errors {
|
||||
background-color: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.fix-errors ul {
|
||||
margin: 0.5rem 0 0 1rem;
|
||||
}
|
||||
|
||||
.missing-episodes {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.loading::before {
|
||||
content: "⏳ ";
|
||||
}
|
||||
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
border: 1px solid #c3e6cb;
|
||||
color: #155724;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
border: 1px solid #f5c6cb;
|
||||
color: #721c24;
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
/* Make text areas more distinctive */
|
||||
textarea {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 0.875rem;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
/* Button variations for admin tools */
|
||||
.btn.btn-info {
|
||||
background-color: #17a2b8;
|
||||
border-color: #17a2b8;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn.btn-info:hover {
|
||||
background-color: #138496;
|
||||
border-color: #117a8b;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for admin tools */
|
||||
@media (max-width: 768px) {
|
||||
.table-container {
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.results-table {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.results-table th,
|
||||
.results-table td {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.admin-tools {
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
@@ -390,6 +390,82 @@
|
||||
<i class="fas fa-sync"></i> Refresh Stats
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Database Query Tool -->
|
||||
<div class="tool-card">
|
||||
<h3><i class="fas fa-terminal"></i> Database Query Tool</h3>
|
||||
<p>Execute custom SQL queries on the NFOGuard database</p>
|
||||
<form id="database-query-form">
|
||||
<div class="form-group">
|
||||
<label>Quick Queries:</label>
|
||||
<select id="quick-query-select" onchange="loadQuickQuery()">
|
||||
<option value="">Select a predefined query...</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>SQL Query:</label>
|
||||
<textarea id="sql-query" rows="4" placeholder="SELECT * FROM episodes WHERE dateadded IS NULL LIMIT 10"></textarea>
|
||||
<small>Query will be automatically limited to 100 results for safety</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Result Limit:</label>
|
||||
<select id="query-limit">
|
||||
<option value="10">10 rows</option>
|
||||
<option value="50">50 rows</option>
|
||||
<option value="100" selected>100 rows</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-info">
|
||||
<i class="fas fa-play"></i> Execute Query
|
||||
</button>
|
||||
</form>
|
||||
<div id="query-results" class="query-results" style="display: none;">
|
||||
<h4>Query Results:</h4>
|
||||
<div id="query-results-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- NFO File Repair Tool -->
|
||||
<div class="tool-card">
|
||||
<h3><i class="fas fa-file-medical"></i> NFO File Repair</h3>
|
||||
<p>Fix missing <dateadded> elements in NFO files</p>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-warning" onclick="checkMissingNFODates()">
|
||||
<i class="fas fa-search"></i> Scan for Missing dateadded in NFO Files
|
||||
</button>
|
||||
</div>
|
||||
<div id="nfo-scan-results" style="display: none;">
|
||||
<div id="nfo-scan-content"></div>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success" onclick="fixAllMissingNFODates()">
|
||||
<i class="fas fa-wrench"></i> Fix All Missing dateadded Elements
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="fixSpecificSeries()">
|
||||
<i class="fas fa-cog"></i> Fix Specific Series
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Database Admin Tools -->
|
||||
<div class="tool-card">
|
||||
<h3><i class="fas fa-user-shield"></i> Database Admin</h3>
|
||||
<p>Advanced database management tools</p>
|
||||
<div class="admin-tools">
|
||||
<button class="btn btn-info" onclick="showEpisodesMissingNFODateadded()">
|
||||
<i class="fas fa-exclamation-triangle"></i> Episodes Missing NFO dateadded
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="exportDatabaseReport()">
|
||||
<i class="fas fa-download"></i> Export Database Report
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="showRecentWebhookActivity()">
|
||||
<i class="fas fa-clock"></i> Recent Webhook Activity
|
||||
</button>
|
||||
</div>
|
||||
<div id="admin-results" class="admin-results" style="display: none;">
|
||||
<div id="admin-results-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -457,6 +533,6 @@
|
||||
<!-- Toast Notifications -->
|
||||
<div class="toast-container" id="toast-container"></div>
|
||||
|
||||
<script src="/static/js/app.js?v=2.8.2-20241025-counts-fix"></script>
|
||||
<script src="/static/js/app.js?v=2.8.2-20241026-admin-tools"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1677,3 +1677,382 @@ function stopScanStatusPolling() {
|
||||
scanStatusInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DATABASE ADMIN TOOLS
|
||||
// ============================================================================
|
||||
|
||||
// Initialize database admin tools
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
initializeDatabaseAdminTools();
|
||||
});
|
||||
|
||||
function initializeDatabaseAdminTools() {
|
||||
// Load quick queries
|
||||
loadQuickQueries();
|
||||
|
||||
// Initialize database query form
|
||||
const dbQueryForm = document.getElementById('database-query-form');
|
||||
if (dbQueryForm) {
|
||||
dbQueryForm.addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
executeDatabaseQuery();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function loadQuickQueries() {
|
||||
try {
|
||||
const response = await apiCall('/api/admin/database/quick-queries');
|
||||
const select = document.getElementById('quick-query-select');
|
||||
|
||||
if (response.success && select) {
|
||||
// Clear existing options except first
|
||||
select.innerHTML = '<option value="">Select a predefined query...</option>';
|
||||
|
||||
response.queries.forEach(query => {
|
||||
const option = document.createElement('option');
|
||||
option.value = query.query;
|
||||
option.textContent = `${query.name} - ${query.description}`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load quick queries:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function loadQuickQuery() {
|
||||
const select = document.getElementById('quick-query-select');
|
||||
const textarea = document.getElementById('sql-query');
|
||||
|
||||
if (select && textarea && select.value) {
|
||||
textarea.value = select.value;
|
||||
}
|
||||
}
|
||||
|
||||
async function executeDatabaseQuery() {
|
||||
const query = document.getElementById('sql-query').value.trim();
|
||||
const limit = document.getElementById('query-limit').value;
|
||||
const resultsDiv = document.getElementById('query-results');
|
||||
const resultsContent = document.getElementById('query-results-content');
|
||||
|
||||
if (!query) {
|
||||
alert('Please enter a SQL query');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Executing query...</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
const response = await apiCall('/api/admin/database/query', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ query, limit: parseInt(limit) })
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
if (response.columns && response.rows) {
|
||||
// SELECT query results
|
||||
let html = `
|
||||
<div class="query-info">
|
||||
<strong>Query:</strong> ${response.query}<br>
|
||||
<strong>Rows returned:</strong> ${response.row_count}
|
||||
</div>
|
||||
<div class="table-container">
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
`;
|
||||
|
||||
response.columns.forEach(col => {
|
||||
html += `<th>${col}</th>`;
|
||||
});
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
response.rows.forEach(row => {
|
||||
html += '<tr>';
|
||||
response.columns.forEach(col => {
|
||||
const value = row[col];
|
||||
html += `<td>${value !== null ? value : '<em>NULL</em>'}</td>`;
|
||||
});
|
||||
html += '</tr>';
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
// Non-SELECT query
|
||||
resultsContent.innerHTML = `
|
||||
<div class="query-info success">
|
||||
<strong>Query:</strong> ${response.query}<br>
|
||||
<strong>Result:</strong> ${response.message}<br>
|
||||
<strong>Rows affected:</strong> ${response.rows_affected}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
resultsContent.innerHTML = `
|
||||
<div class="query-info error">
|
||||
<strong>Error:</strong> ${response.error}<br>
|
||||
<strong>Query:</strong> ${response.query}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `
|
||||
<div class="query-info error">
|
||||
<strong>Error:</strong> Failed to execute query: ${error.message}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// NFO File Repair Functions
|
||||
async function checkMissingNFODates() {
|
||||
const resultsDiv = document.getElementById('nfo-scan-results');
|
||||
const resultsContent = document.getElementById('nfo-scan-content');
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Scanning NFO files for missing dateadded elements...</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
const response = await apiCall('/api/admin/nfo/missing-dateadded');
|
||||
|
||||
if (response.success) {
|
||||
let html = `
|
||||
<div class="scan-summary">
|
||||
<h4>NFO Scan Results</h4>
|
||||
<p><strong>Total episodes checked:</strong> ${response.total_episodes_checked}</p>
|
||||
<p><strong>Episodes missing dateadded in NFO:</strong> ${response.missing_dateadded_count}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (response.missing_dateadded_count > 0) {
|
||||
html += `
|
||||
<div class="missing-episodes">
|
||||
<h5>Episodes with missing dateadded (showing first 50):</h5>
|
||||
<div class="table-container">
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Series</th>
|
||||
<th>Season</th>
|
||||
<th>Episode</th>
|
||||
<th>Database Date</th>
|
||||
<th>Source</th>
|
||||
<th>NFO Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
response.episodes.forEach(ep => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${ep.imdb_id}</td>
|
||||
<td>${ep.season}</td>
|
||||
<td>${ep.episode}</td>
|
||||
<td>${new Date(ep.dateadded).toLocaleString()}</td>
|
||||
<td>${ep.source}</td>
|
||||
<td><small>${ep.nfo_path}</small></td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div></div>';
|
||||
} else {
|
||||
html += '<div class="success">✅ All NFO files have dateadded elements!</div>';
|
||||
}
|
||||
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
resultsContent.innerHTML = `<div class="error">Error: ${response.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `<div class="error">Failed to scan NFO files: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fixAllMissingNFODates() {
|
||||
if (!confirm('This will update all NFO files that are missing dateadded elements. Continue?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultsContent = document.getElementById('nfo-scan-content');
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Fixing all missing dateadded elements in NFO files...</div>';
|
||||
|
||||
const response = await apiCall('/api/admin/nfo/bulk-update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ fix_all: true })
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
let html = `
|
||||
<div class="fix-summary success">
|
||||
<h4>NFO Fix Results</h4>
|
||||
<p>✅ <strong>${response.updated_count}</strong> NFO files updated successfully</p>
|
||||
<p><strong>Total episodes processed:</strong> ${response.total_episodes}</p>
|
||||
<p><strong>Message:</strong> ${response.message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (response.errors && response.errors.length > 0) {
|
||||
html += `
|
||||
<div class="fix-errors">
|
||||
<h5>Errors (first 10):</h5>
|
||||
<ul>
|
||||
`;
|
||||
response.errors.forEach(error => {
|
||||
html += `<li>${error}</li>`;
|
||||
});
|
||||
html += '</ul></div>';
|
||||
}
|
||||
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
resultsContent.innerHTML = `<div class="error">Error: ${response.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `<div class="error">Failed to fix NFO files: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fixSpecificSeries() {
|
||||
const imdbIds = prompt('Enter IMDb IDs to fix (comma-separated):\nExample: tt9111220,tt12327578');
|
||||
|
||||
if (!imdbIds) return;
|
||||
|
||||
const idArray = imdbIds.split(',').map(id => id.trim()).filter(id => id);
|
||||
|
||||
if (idArray.length === 0) {
|
||||
alert('Please enter valid IMDb IDs');
|
||||
return;
|
||||
}
|
||||
|
||||
const resultsContent = document.getElementById('nfo-scan-content');
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Fixing NFO files for specified series...</div>';
|
||||
|
||||
const response = await apiCall('/api/admin/nfo/bulk-update', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ imdb_ids: idArray })
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
let html = `
|
||||
<div class="fix-summary success">
|
||||
<h4>NFO Fix Results for Specific Series</h4>
|
||||
<p>✅ <strong>${response.updated_count}</strong> NFO files updated successfully</p>
|
||||
<p><strong>Series processed:</strong> ${idArray.join(', ')}</p>
|
||||
<p><strong>Message:</strong> ${response.message}</p>
|
||||
</div>
|
||||
`;
|
||||
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
resultsContent.innerHTML = `<div class="error">Error: ${response.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `<div class="error">Failed to fix NFO files: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
// Database Admin Functions
|
||||
async function showEpisodesMissingNFODateadded() {
|
||||
const resultsDiv = document.getElementById('admin-results');
|
||||
const resultsContent = document.getElementById('admin-results-content');
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Checking for episodes missing NFO dateadded...</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
const response = await apiCall('/api/admin/nfo/missing-dateadded');
|
||||
|
||||
if (response.success) {
|
||||
const html = `
|
||||
<h4>Episodes Missing NFO dateadded</h4>
|
||||
<p><strong>Found:</strong> ${response.missing_dateadded_count} episodes missing dateadded in NFO files</p>
|
||||
<p><strong>Total checked:</strong> ${response.total_episodes_checked} episodes</p>
|
||||
${response.missing_dateadded_count > 0 ?
|
||||
'<p><strong>Recommendation:</strong> Use the "NFO File Repair" tool to fix these issues.</p>' :
|
||||
'<p>✅ All NFO files are properly maintained!</p>'
|
||||
}
|
||||
`;
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
resultsContent.innerHTML = `<div class="error">Error: ${response.message}</div>`;
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `<div class="error">Failed to check NFO files: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function exportDatabaseReport() {
|
||||
alert('Database export functionality will be implemented in a future update.');
|
||||
}
|
||||
|
||||
async function showRecentWebhookActivity() {
|
||||
const resultsDiv = document.getElementById('admin-results');
|
||||
const resultsContent = document.getElementById('admin-results-content');
|
||||
|
||||
try {
|
||||
resultsContent.innerHTML = '<div class="loading">Loading recent webhook activity...</div>';
|
||||
resultsDiv.style.display = 'block';
|
||||
|
||||
const response = await apiCall('/api/admin/database/query', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
query: "SELECT * FROM episodes WHERE last_updated > NOW() - INTERVAL '24 hours' ORDER BY last_updated DESC",
|
||||
limit: 20
|
||||
})
|
||||
});
|
||||
|
||||
if (response.success && response.rows) {
|
||||
let html = `
|
||||
<h4>Recent Webhook Activity (Last 24 Hours)</h4>
|
||||
<p><strong>Episodes processed:</strong> ${response.row_count}</p>
|
||||
<div class="table-container">
|
||||
<table class="results-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>IMDb ID</th>
|
||||
<th>Season</th>
|
||||
<th>Episode</th>
|
||||
<th>Date Added</th>
|
||||
<th>Source</th>
|
||||
<th>Last Updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
response.rows.forEach(row => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${row.imdb_id}</td>
|
||||
<td>${row.season}</td>
|
||||
<td>${row.episode}</td>
|
||||
<td>${row.dateadded ? new Date(row.dateadded).toLocaleString() : 'None'}</td>
|
||||
<td>${row.source}</td>
|
||||
<td>${new Date(row.last_updated).toLocaleString()}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
html += '</tbody></table></div>';
|
||||
resultsContent.innerHTML = html;
|
||||
} else {
|
||||
resultsContent.innerHTML = '<p>No recent webhook activity found.</p>';
|
||||
}
|
||||
} catch (error) {
|
||||
resultsContent.innerHTML = `<div class="error">Failed to load webhook activity: ${error.message}</div>`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user