rapid interations of server side app and firefox extension

This commit is contained in:
Bryan Van Deusen
2026-01-24 22:52:51 -05:00
commit b9b8048a2d
81 changed files with 9675 additions and 0 deletions
+283
View File
@@ -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>