mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-07-22 06:20:09 +00:00
- [x] Implement GORM V2 metadata store with SQLite, PostgreSQL, and MySQL support - [x] Add database migration system using gormigrate for schema versioning - [x] Create migration CLI tool with support for migrate, rollback, and status commands - [x] Add Docker support for migration container (Dockerfile.migrate) - [x] Implement automatic partition management for PostgreSQL time-series tables - [x] Add background aggregation worker for download statistics - [x] Support connection pooling configuration (max_open_conns, max_idle_conns, conn_max_lifetime) - [x] Add blocking mechanism based on vulnerability thresholds in stats and handlers - [x] Update Helm charts with migration init containers and multi-database configuration - [x] Replace deprecated SQLite store with optimized GORM implementation - [x] Add comprehensive integration tests for MySQL and PostgreSQL - [x] Update frontend to display blocked packages and storage utilization - [x] Add goreleaser configuration for migrate binary and container image - [x] Update configuration examples with database backend options and recommendations
142 lines
5.0 KiB
Vue
142 lines
5.0 KiB
Vue
<template>
|
|
<div class="flex items-center gap-2">
|
|
<!-- Blocked Icon (if package exceeds thresholds) -->
|
|
<span
|
|
v-if="isBlocked"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-red-600 text-white border border-red-700"
|
|
title="Download blocked - exceeds vulnerability thresholds"
|
|
>
|
|
<i class="fas fa-hand mr-1"></i>
|
|
BLOCKED
|
|
</span>
|
|
|
|
<!-- Critical Vulnerabilities -->
|
|
<button
|
|
v-if="counts.critical > 0"
|
|
@click="handleClick('critical')"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-red-100 text-red-800 hover:bg-red-200 border border-red-300 transition-colors cursor-pointer"
|
|
:title="`${counts.critical} critical vulnerabilities - click for details`"
|
|
>
|
|
<i class="fas fa-shield-virus mr-1"></i>
|
|
CRITICAL: {{ counts.critical }}
|
|
</button>
|
|
|
|
<!-- High Vulnerabilities -->
|
|
<button
|
|
v-if="counts.high > 0"
|
|
@click="handleClick('high')"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-orange-100 text-orange-800 hover:bg-orange-200 border border-orange-300 transition-colors cursor-pointer"
|
|
:title="`${counts.high} high severity vulnerabilities - click for details`"
|
|
>
|
|
<i class="fas fa-exclamation-triangle mr-1"></i>
|
|
HIGH: {{ counts.high }}
|
|
</button>
|
|
|
|
<!-- Moderate Vulnerabilities -->
|
|
<button
|
|
v-if="counts.moderate > 0"
|
|
@click="handleClick('moderate')"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-yellow-100 text-yellow-800 hover:bg-yellow-200 border border-yellow-300 transition-colors cursor-pointer"
|
|
:title="`${counts.moderate} moderate severity vulnerabilities - click for details`"
|
|
>
|
|
<i class="fas fa-exclamation-circle mr-1"></i>
|
|
MODERATE: {{ counts.moderate }}
|
|
</button>
|
|
|
|
<!-- Low Vulnerabilities -->
|
|
<button
|
|
v-if="counts.low > 0"
|
|
@click="handleClick('low')"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-blue-100 text-blue-800 hover:bg-blue-200 border border-blue-300 transition-colors cursor-pointer"
|
|
:title="`${counts.low} low severity vulnerabilities - click for details`"
|
|
>
|
|
<i class="fas fa-info-circle mr-1"></i>
|
|
LOW: {{ counts.low }}
|
|
</button>
|
|
|
|
<!-- Clean Badge (no vulnerabilities) -->
|
|
<span
|
|
v-if="status === 'clean' && total === 0"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-green-100 text-green-800 border border-green-300"
|
|
:title="scannedAt ? `No vulnerabilities found - Scanned ${formatTimestamp(scannedAt)}` : 'No vulnerabilities found'"
|
|
>
|
|
<i class="fas fa-check-circle mr-1"></i>
|
|
CLEAN
|
|
<span v-if="scannedAt" class="ml-1 text-[10px] opacity-70">
|
|
({{ formatRelativeTime(scannedAt) }})
|
|
</span>
|
|
</span>
|
|
|
|
<!-- Pending Badge -->
|
|
<span
|
|
v-if="status === 'pending'"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-700 border border-gray-300"
|
|
title="Security scan in progress"
|
|
>
|
|
<i class="fas fa-spinner fa-spin mr-1"></i>
|
|
SCANNING...
|
|
</span>
|
|
|
|
<!-- Not Scanned Badge -->
|
|
<span
|
|
v-if="status === 'not_scanned' || !scanned"
|
|
class="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-gray-100 text-gray-600 border border-gray-300"
|
|
title="Not yet scanned for vulnerabilities"
|
|
>
|
|
<i class="fas fa-question-circle mr-1"></i>
|
|
NOT SCANNED
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed } from 'vue'
|
|
import type { VulnerabilityCounts } from '../stores/packages'
|
|
|
|
interface Props {
|
|
scanned?: boolean
|
|
status?: 'clean' | 'vulnerable' | 'pending' | 'not_scanned'
|
|
counts?: VulnerabilityCounts
|
|
total?: number
|
|
scannedAt?: string // ISO 8601 timestamp
|
|
isBlocked?: boolean // Whether download is blocked due to vulnerabilities
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
scanned: false,
|
|
status: 'not_scanned',
|
|
total: 0,
|
|
isBlocked: false,
|
|
})
|
|
|
|
const emit = defineEmits<{
|
|
click: [severity: string]
|
|
}>()
|
|
|
|
const counts = computed(() => props.counts || { critical: 0, high: 0, moderate: 0, low: 0 })
|
|
|
|
function handleClick(severity: string) {
|
|
emit('click', severity)
|
|
}
|
|
|
|
function formatTimestamp(timestamp: string): string {
|
|
const date = new Date(timestamp)
|
|
return date.toLocaleString()
|
|
}
|
|
|
|
function formatRelativeTime(timestamp: string): string {
|
|
const date = new Date(timestamp)
|
|
const now = new Date()
|
|
const diffMs = now.getTime() - date.getTime()
|
|
const diffMins = Math.floor(diffMs / 60000)
|
|
const diffHours = Math.floor(diffMs / 3600000)
|
|
const diffDays = Math.floor(diffMs / 86400000)
|
|
|
|
if (diffMins < 1) return 'just now'
|
|
if (diffMins < 60) return `${diffMins}m ago`
|
|
if (diffHours < 24) return `${diffHours}h ago`
|
|
if (diffDays < 7) return `${diffDays}d ago`
|
|
return formatTimestamp(timestamp).split(',')[0] // Just the date part
|
|
}
|
|
</script>
|