rapid interations of server side app and firefox extension
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-row>
|
||||
<!-- Stats Cards -->
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-avatar color="primary" size="48" class="mr-4">
|
||||
<v-icon>mdi-account-multiple</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="text-h4">{{ sourcesStore.sources.length }}</div>
|
||||
<div class="text-caption">Total Sources</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-avatar color="success" size="48" class="mr-4">
|
||||
<v-icon>mdi-check-circle</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="text-h4">{{ stats?.completed || 0 }}</div>
|
||||
<div class="text-caption">Completed Downloads</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-avatar color="error" size="48" class="mr-4">
|
||||
<v-icon>mdi-alert-circle</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="text-h4">{{ stats?.failed || 0 }}</div>
|
||||
<div class="text-caption">Failed Downloads</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="3">
|
||||
<v-card>
|
||||
<v-card-text class="d-flex align-center">
|
||||
<v-avatar color="warning" size="48" class="mr-4">
|
||||
<v-icon>mdi-clock-outline</v-icon>
|
||||
</v-avatar>
|
||||
<div>
|
||||
<div class="text-h4">{{ stats?.pending || 0 }}</div>
|
||||
<div class="text-caption">Pending Downloads</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-4">
|
||||
<!-- Downloads by Platform -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>Downloads by Platform</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list v-if="stats?.by_platform">
|
||||
<v-list-item
|
||||
v-for="(count, platform) in stats.by_platform"
|
||||
:key="platform"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="getPlatformColor(platform)">
|
||||
{{ getPlatformIcon(platform) }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title class="text-capitalize">
|
||||
{{ platform }}
|
||||
</v-list-item-title>
|
||||
<template v-slot:append>
|
||||
<v-chip size="small">{{ count }}</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
No download data yet
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<!-- Recent Activity -->
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>Recent Downloads</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list v-if="recentDownloads.length">
|
||||
<v-list-item
|
||||
v-for="download in recentDownloads"
|
||||
:key="download.id"
|
||||
>
|
||||
<template v-slot:prepend>
|
||||
<v-icon :color="getStatusColor(download.status)">
|
||||
{{ getStatusIcon(download.status) }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>
|
||||
{{ truncate(download.url, 40) }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
{{ formatDate(download.created_at) }}
|
||||
</v-list-item-subtitle>
|
||||
<template v-slot:append>
|
||||
<v-chip
|
||||
:color="getStatusColor(download.status)"
|
||||
size="small"
|
||||
variant="tonal"
|
||||
>
|
||||
{{ download.status }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
No recent downloads
|
||||
</div>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-btn variant="text" to="/downloads">View All Downloads</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-4">
|
||||
<!-- Sources needing attention -->
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>
|
||||
Sources Needing Attention
|
||||
<v-chip class="ml-2" color="error" size="small" v-if="sourcesWithErrors.length">
|
||||
{{ sourcesWithErrors.length }}
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
<v-card-text>
|
||||
<v-table v-if="sourcesWithErrors.length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Platform</th>
|
||||
<th>Error Count</th>
|
||||
<th>Last Check</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="source in sourcesWithErrors" :key="source.id">
|
||||
<td>{{ source.subscription_name || source.subscription?.name || 'Unknown' }}</td>
|
||||
<td class="text-capitalize">{{ source.platform }}</td>
|
||||
<td>
|
||||
<v-chip color="error" size="small">
|
||||
{{ source.error_count }} errors
|
||||
</v-chip>
|
||||
</td>
|
||||
<td>{{ formatDate(source.last_check) }}</td>
|
||||
<td>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
color="primary"
|
||||
@click="retrySource(source)"
|
||||
>
|
||||
Retry
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="small"
|
||||
variant="text"
|
||||
:to="`/sources/${source.id}/edit`"
|
||||
>
|
||||
Edit
|
||||
</v-btn>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
<div v-else class="text-center text-grey py-4">
|
||||
<v-icon size="48" class="mb-2">mdi-check-circle-outline</v-icon>
|
||||
<div>All sources are healthy!</div>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useSourcesStore } from '../stores/sources'
|
||||
import { useDownloadsStore } from '../stores/downloads'
|
||||
import { useNotificationStore } from '../stores/notifications'
|
||||
|
||||
const sourcesStore = useSourcesStore()
|
||||
const downloadsStore = useDownloadsStore()
|
||||
const notifications = useNotificationStore()
|
||||
|
||||
const stats = computed(() => downloadsStore.stats)
|
||||
const recentDownloads = computed(() => downloadsStore.downloads.slice(0, 5))
|
||||
const sourcesWithErrors = computed(() =>
|
||||
sourcesStore.sources.filter(s => s.error_count > 0).slice(0, 5)
|
||||
)
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([
|
||||
sourcesStore.fetchSources(),
|
||||
downloadsStore.fetchStats(),
|
||||
downloadsStore.fetchDownloads({ per_page: 5 }),
|
||||
])
|
||||
})
|
||||
|
||||
function getPlatformIcon(platform) {
|
||||
const icons = {
|
||||
patreon: 'mdi-patreon',
|
||||
subscribestar: 'mdi-star',
|
||||
hentaifoundry: 'mdi-palette',
|
||||
discord: 'mdi-discord',
|
||||
}
|
||||
return icons[platform] || 'mdi-web'
|
||||
}
|
||||
|
||||
function getPlatformColor(platform) {
|
||||
const colors = {
|
||||
patreon: '#FF424D',
|
||||
subscribestar: '#FFD700',
|
||||
hentaifoundry: '#9C27B0',
|
||||
discord: '#5865F2',
|
||||
}
|
||||
return colors[platform] || 'grey'
|
||||
}
|
||||
|
||||
function getStatusIcon(status) {
|
||||
const icons = {
|
||||
completed: 'mdi-check-circle',
|
||||
failed: 'mdi-alert-circle',
|
||||
running: 'mdi-loading mdi-spin',
|
||||
pending: 'mdi-clock-outline',
|
||||
skipped: 'mdi-skip-next',
|
||||
}
|
||||
return icons[status] || 'mdi-help-circle'
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
const colors = {
|
||||
completed: 'success',
|
||||
failed: 'error',
|
||||
running: 'info',
|
||||
pending: 'warning',
|
||||
skipped: 'grey',
|
||||
}
|
||||
return colors[status] || 'grey'
|
||||
}
|
||||
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return 'Never'
|
||||
return new Date(dateStr).toLocaleString()
|
||||
}
|
||||
|
||||
function truncate(str, length) {
|
||||
if (!str) return ''
|
||||
if (str.length <= length) return str
|
||||
return str.substring(0, length) + '...'
|
||||
}
|
||||
|
||||
async function retrySource(source) {
|
||||
try {
|
||||
await sourcesStore.triggerCheck(source.id)
|
||||
notifications.success(`Queued check for ${source.subscription_name || source.platform}`)
|
||||
} catch (error) {
|
||||
notifications.error(`Failed to queue check: ${error.message}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user