feat: enhance media management with CRUD operations and runtime tracking in admin UI
This commit is contained in:
1 parent
47740e3f4a
commit
7cfddfc7ef
6 files changed
+535
-103
No files matched your search
+128
-9
@@ -38,7 +38,7 @@
|
||||
<template x-if="currentTab === 'dashboard'">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold mb-8">Dashboard</h1>
|
||||
<div class="grid grid-cols-3 gap-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-6">
|
||||
<div class="bg-slate-800 p-6 rounded-3xl">
|
||||
<div class="text-emerald-400 text-sm">Concurrent Viewers</div>
|
||||
<div class="text-5xl font-bold mt-2" x-text="stats.viewers"></div>
|
||||
@@ -51,6 +51,13 @@
|
||||
<div class="text-amber-400 text-sm">Moderation Actions</div>
|
||||
<div class="text-5xl font-bold mt-2" x-text="stats.moderationActions"></div>
|
||||
</div>
|
||||
<div class="bg-slate-800 p-6 rounded-3xl">
|
||||
<div class="text-violet-400 text-sm">Media Runtime</div>
|
||||
<div class="text-3xl font-bold mt-2" x-text="stats.mediaTotalRuntime"></div>
|
||||
<div class="text-xs text-slate-400 mt-2">
|
||||
<span x-text="stats.mediaCount"></span> files
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 text-sm text-slate-400" x-text="statusMessage"></div>
|
||||
</div>
|
||||
@@ -82,21 +89,34 @@
|
||||
<thead class="text-slate-400 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="text-left py-3 pr-4 font-medium">File</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Title</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Runtime</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Status</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Size</th>
|
||||
<th class="text-left py-3 font-medium">Modified</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Modified</th>
|
||||
<th class="text-left py-3 font-medium">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="media.files.length === 0">
|
||||
<tr>
|
||||
<td colspan="3" class="py-6 text-slate-500">No media files available.</td>
|
||||
<td colspan="7" class="py-6 text-slate-500">No media files available.</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="file in media.files" :key="file.name">
|
||||
<template x-for="file in media.files" :key="file.id || file.name">
|
||||
<tr class="border-b border-slate-800">
|
||||
<td class="py-3 pr-4" x-text="file.name"></td>
|
||||
<td class="py-3 pr-4" x-text="file.title || '-' "></td>
|
||||
<td class="py-3 pr-4 font-mono text-xs" x-text="file.runtime || '00:00:00'"></td>
|
||||
<td class="py-3 pr-4" x-text="file.status || 'unknown'"></td>
|
||||
<td class="py-3 pr-4" x-text="file.size"></td>
|
||||
<td class="py-3 font-mono text-xs" x-text="file.modifiedAt"></td>
|
||||
<td class="py-3 pr-4 font-mono text-xs" x-text="file.modifiedAt"></td>
|
||||
<td class="py-3">
|
||||
<div class="flex gap-2">
|
||||
<button @click="editMedia(file)" class="bg-slate-700 hover:bg-slate-600 px-3 py-1 rounded-lg text-xs">Edit</button>
|
||||
<button @click="deleteMedia(file)" class="bg-rose-700 hover:bg-rose-600 px-3 py-1 rounded-lg text-xs">Delete</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
@@ -116,25 +136,33 @@
|
||||
<button @click="refreshSchedule()" class="bg-slate-700 hover:bg-slate-600 px-4 py-2 rounded-xl text-sm">Reload schedule</button>
|
||||
</div>
|
||||
<div class="bg-slate-800 rounded-3xl p-6">
|
||||
<div class="mb-4 text-sm text-slate-400">
|
||||
<span x-text="schedule.matchedEntries"></span>/<span x-text="schedule.totalEntries"></span> matched
|
||||
• total runtime <span class="font-mono" x-text="schedule.totalRuntime"></span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="text-slate-400 border-b border-slate-700">
|
||||
<tr>
|
||||
<th class="text-left py-3 pr-4 font-medium">#</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Label</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Runtime</th>
|
||||
<th class="text-left py-3 pr-4 font-medium">Status</th>
|
||||
<th class="text-left py-3 font-medium">Path</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-if="schedule.entries.length === 0">
|
||||
<tr>
|
||||
<td colspan="3" class="py-6 text-slate-500">No schedule entries found.</td>
|
||||
<td colspan="5" class="py-6 text-slate-500">No schedule entries found.</td>
|
||||
</tr>
|
||||
</template>
|
||||
<template x-for="entry in schedule.entries" :key="entry.index + ':' + entry.path">
|
||||
<tr class="border-b border-slate-800">
|
||||
<td class="py-3 pr-4" x-text="entry.index"></td>
|
||||
<td class="py-3 pr-4" x-text="entry.label"></td>
|
||||
<td class="py-3 pr-4 font-mono text-xs" x-text="entry.runtime || '00:00:00'"></td>
|
||||
<td class="py-3 pr-4" x-text="entry.status || 'missing'"></td>
|
||||
<td class="py-3 font-mono text-xs" x-text="entry.path"></td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -448,7 +476,9 @@
|
||||
stats: {
|
||||
viewers: 0,
|
||||
storedMessages: 0,
|
||||
moderationActions: 0
|
||||
moderationActions: 0,
|
||||
mediaCount: 0,
|
||||
mediaTotalRuntime: '00:00:00'
|
||||
},
|
||||
system: {
|
||||
redisEnabled: false,
|
||||
@@ -467,6 +497,8 @@
|
||||
messagesLast24Hours: 0,
|
||||
distinctRooms: 0,
|
||||
moderationActions: 0,
|
||||
mediaCount: 0,
|
||||
mediaTotalRuntime: '00:00:00',
|
||||
topRooms: [],
|
||||
generatedAt: 'loading...',
|
||||
status: 'Loading analytics...',
|
||||
@@ -474,6 +506,9 @@
|
||||
},
|
||||
schedule: {
|
||||
entries: [],
|
||||
totalEntries: 0,
|
||||
matchedEntries: 0,
|
||||
totalRuntime: '00:00:00',
|
||||
status: 'Loading schedule...',
|
||||
error: false
|
||||
},
|
||||
@@ -520,6 +555,8 @@
|
||||
this.stats.viewers = overview.viewers;
|
||||
this.stats.storedMessages = overview.storedMessages;
|
||||
this.stats.moderationActions = overview.moderationActions;
|
||||
this.stats.mediaCount = overview.mediaCount || 0;
|
||||
this.stats.mediaTotalRuntime = overview.mediaTotalRuntime || '00:00:00';
|
||||
this.system.redisEnabled = overview.redisEnabled;
|
||||
this.system.redisClients = overview.redisClients;
|
||||
this.system.redisMemoryUsed = overview.redisMemoryUsed;
|
||||
@@ -549,6 +586,8 @@
|
||||
this.analytics.messagesLast24Hours = payload.messagesLast24Hours;
|
||||
this.analytics.distinctRooms = payload.distinctRooms;
|
||||
this.analytics.moderationActions = payload.moderationActions;
|
||||
this.analytics.mediaCount = payload.mediaCount || 0;
|
||||
this.analytics.mediaTotalRuntime = payload.mediaTotalRuntime || '00:00:00';
|
||||
this.analytics.topRooms = payload.topRooms || [];
|
||||
this.analytics.generatedAt = payload.generatedAt;
|
||||
this.analytics.status = 'Analytics loaded.';
|
||||
@@ -589,7 +628,10 @@
|
||||
|
||||
const payload = await response.json();
|
||||
this.schedule.entries = payload.entries || [];
|
||||
this.schedule.status = 'Schedule loaded.';
|
||||
this.schedule.totalEntries = payload.totalEntries || 0;
|
||||
this.schedule.matchedEntries = payload.matchedEntries || 0;
|
||||
this.schedule.totalRuntime = payload.totalRuntime || '00:00:00';
|
||||
this.schedule.status = `Schedule loaded. Total runtime ${this.schedule.totalRuntime}.`;
|
||||
} catch (error) {
|
||||
console.error('Failed to load schedule', error);
|
||||
this.schedule.status = 'Failed to load schedule.';
|
||||
@@ -641,6 +683,83 @@
|
||||
this.media.error = true;
|
||||
}
|
||||
},
|
||||
async editMedia(file) {
|
||||
if (!file.id) {
|
||||
this.media.status = 'Cannot edit media item without id.';
|
||||
this.media.error = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const title = window.prompt('Title', file.title || file.name || '');
|
||||
if (title === null) {
|
||||
return;
|
||||
}
|
||||
const status = window.prompt('Status', file.status || 'ready');
|
||||
if (status === null) {
|
||||
return;
|
||||
}
|
||||
const tags = window.prompt('Tags (comma-separated)', file.tags || '');
|
||||
if (tags === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.media.error = false;
|
||||
this.media.status = `Saving metadata for ${file.name}...`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/media/${file.id}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ title, status, tags })
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
this.media.status = 'Media metadata updated.';
|
||||
await this.refreshMediaLibrary();
|
||||
await this.refreshOverview();
|
||||
await this.refreshSchedule();
|
||||
} catch (error) {
|
||||
console.error('Failed to update media metadata', error);
|
||||
this.media.status = 'Failed to update media metadata.';
|
||||
this.media.error = true;
|
||||
}
|
||||
},
|
||||
async deleteMedia(file) {
|
||||
if (!file.id) {
|
||||
this.media.status = 'Cannot delete media item without id.';
|
||||
this.media.error = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!window.confirm(`Delete ${file.name}? This removes file and metadata.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.media.error = false;
|
||||
this.media.status = `Deleting ${file.name}...`;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/media/${file.id}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
this.media.status = `${file.name} deleted.`;
|
||||
await this.refreshMediaLibrary();
|
||||
await this.refreshOverview();
|
||||
await this.refreshSchedule();
|
||||
} catch (error) {
|
||||
console.error('Failed to delete media', error);
|
||||
this.media.status = 'Failed to delete media file.';
|
||||
this.media.error = true;
|
||||
}
|
||||
},
|
||||
async savePlaylist() {
|
||||
this.playlist.saving = true;
|
||||
this.playlist.status = 'Saving playlist...';
|
||||
@@ -799,7 +918,7 @@
|
||||
}
|
||||
|
||||
function runVacuum() {
|
||||
alert('VACUUM executed on SQLite database (per ADR-006). Audit log entry created.');
|
||||
alert('Database maintenance action placeholder. Use storage backend-specific operations from server-side APIs.');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in new issue
Block a user