Release to the world.

This commit is contained in:
2025-12-19 02:17:02 +00:00
parent d7c20cea54
commit 85e1dfa7f3
13 changed files with 858 additions and 11 deletions
+6 -1
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useSSE, useStats, useTimeline } from '@/composables'
import { useSSE, useStats, useTimeline, useUpdate } from '@/composables'
import Header from '@/components/Header.vue'
import StatsCards from '@/components/StatsCards.vue'
import FilterTabs from '@/components/FilterTabs.vue'
@@ -10,6 +10,7 @@ import Sidebar from '@/components/Sidebar.vue'
// Composables
const { isConnected, isProcessing, queueDepth, lastEvent } = useSSE()
const { stats } = useStats()
const { updateInfo, updateStatus, isUpdating, applyUpdate } = useUpdate()
const {
filteredItems,
loading,
@@ -41,7 +42,11 @@ watch(lastEvent, (event) => {
<Header
:is-connected="isConnected"
:is-processing="isProcessing"
:update-info="updateInfo"
:update-status="updateStatus"
:is-updating="isUpdating"
@refresh="refresh"
@apply-update="applyUpdate"
/>
<!-- Main Content -->
+95
View File
@@ -1,12 +1,25 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { UpdateInfo, UpdateStatus } from '@/composables/useUpdate'
defineProps<{
isConnected: boolean
isProcessing: boolean
updateInfo: UpdateInfo | null
updateStatus: UpdateStatus
isUpdating: boolean
}>()
const emit = defineEmits<{
refresh: []
applyUpdate: []
}>()
const showUpdateModal = ref(false)
const reloadPage = () => {
globalThis.location.reload()
}
</script>
<template>
@@ -26,6 +39,41 @@ const emit = defineEmits<{
<!-- Status & Actions -->
<div class="flex items-center gap-4">
<!-- Update Available Indicator -->
<div v-if="updateInfo?.available && !isUpdating && updateStatus.state === 'idle'" class="relative">
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-amber-500/20 border border-amber-500/50 text-amber-400 hover:bg-amber-500/30 transition-colors text-sm"
@click="showUpdateModal = true"
>
<i class="fas fa-arrow-circle-up" />
<span>v{{ updateInfo.latest_version }}</span>
</button>
</div>
<!-- Update In Progress -->
<div v-else-if="isUpdating" class="flex items-center gap-2 text-amber-400 text-sm">
<i class="fas fa-spinner animate-spin" />
<span>{{ updateStatus.message || 'Updating...' }}</span>
<span class="text-slate-500">{{ Math.round(updateStatus.progress * 100) }}%</span>
</div>
<!-- Update Complete -->
<div v-else-if="updateStatus.state === 'done'" class="flex items-center gap-2">
<button
class="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-green-500/20 border border-green-500/50 text-green-400 hover:bg-green-500/30 transition-colors text-sm"
@click="reloadPage"
>
<i class="fas fa-check-circle" />
<span>Restart</span>
</button>
</div>
<!-- Update Error -->
<div v-else-if="updateStatus.state === 'error'" class="flex items-center gap-2 text-red-400 text-sm" :title="updateStatus.error">
<i class="fas fa-exclamation-circle" />
<span>Update failed</span>
</div>
<!-- Connection Status -->
<div class="flex items-center gap-2">
<span
@@ -51,5 +99,52 @@ const emit = defineEmits<{
</div>
</div>
</div>
<!-- Update Modal -->
<Teleport to="body">
<div v-if="showUpdateModal" class="fixed inset-0 z-50 flex items-center justify-center p-4">
<!-- Backdrop -->
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="showUpdateModal = false" />
<!-- Modal -->
<div class="relative glass border border-white/10 rounded-2xl p-6 max-w-md w-full shadow-2xl">
<button
class="absolute top-4 right-4 text-slate-400 hover:text-white"
@click="showUpdateModal = false"
>
<i class="fas fa-times" />
</button>
<div class="text-center mb-6">
<div class="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
<i class="fas fa-arrow-circle-up text-3xl text-white" />
</div>
<h3 class="text-xl font-bold text-white mb-1">Update Available</h3>
<p class="text-slate-400 text-sm">
v{{ updateInfo?.current_version }} → v{{ updateInfo?.latest_version }}
</p>
</div>
<div class="space-y-3">
<button
class="w-full py-3 rounded-xl bg-amber-500 text-slate-900 font-semibold hover:bg-amber-400 transition-colors"
@click="emit('applyUpdate'); showUpdateModal = false"
>
Update Now
</button>
<button
class="w-full py-3 rounded-xl bg-white/5 text-slate-400 hover:bg-white/10 hover:text-white transition-colors"
@click="showUpdateModal = false"
>
Later
</button>
</div>
<p class="text-center text-slate-500 text-xs mt-4">
Updates are verified with cosign signatures
</p>
</div>
</div>
</Teleport>
</header>
</template>
+1
View File
@@ -1,3 +1,4 @@
export { useSSE } from './useSSE'
export { useStats } from './useStats'
export { useTimeline } from './useTimeline'
export { useUpdate } from './useUpdate'
+120
View File
@@ -0,0 +1,120 @@
import { ref, onMounted, onUnmounted } from 'vue'
export interface UpdateInfo {
available: boolean
current_version: string
latest_version: string
release_notes?: string
published_at?: string
}
export interface UpdateStatus {
state: 'idle' | 'checking' | 'downloading' | 'verifying' | 'applying' | 'done' | 'error'
progress: number
message: string
error?: string
}
const CHECK_INTERVAL = 30 * 60 * 1000 // 30 minutes in milliseconds
export function useUpdate() {
const updateInfo = ref<UpdateInfo | null>(null)
const updateStatus = ref<UpdateStatus>({ state: 'idle', progress: 0, message: '' })
const isChecking = ref(false)
const isUpdating = ref(false)
let statusInterval: ReturnType<typeof setInterval> | null = null
let checkInterval: ReturnType<typeof setInterval> | null = null
const checkForUpdate = async () => {
isChecking.value = true
try {
const response = await fetch('/api/update/check')
if (response.ok) {
updateInfo.value = await response.json()
}
} catch (error) {
console.error('Failed to check for updates:', error)
} finally {
isChecking.value = false
}
}
const applyUpdate = async () => {
if (!updateInfo.value?.available) return
isUpdating.value = true
try {
const response = await fetch('/api/update/apply', { method: 'POST' })
if (response.ok) {
// Start polling for status
startStatusPolling()
}
} catch (error) {
console.error('Failed to apply update:', error)
isUpdating.value = false
}
}
const fetchStatus = async () => {
try {
const response = await fetch('/api/update/status')
if (response.ok) {
updateStatus.value = await response.json()
// Stop polling when done or error
if (updateStatus.value.state === 'done' || updateStatus.value.state === 'error') {
stopStatusPolling()
isUpdating.value = false
}
}
} catch (error) {
console.error('Failed to fetch update status:', error)
}
}
const startStatusPolling = () => {
if (statusInterval) return
statusInterval = setInterval(fetchStatus, 1000)
fetchStatus()
}
const stopStatusPolling = () => {
if (statusInterval) {
clearInterval(statusInterval)
statusInterval = null
}
}
const startPeriodicCheck = () => {
if (checkInterval) return
// Check every hour
checkInterval = setInterval(checkForUpdate, CHECK_INTERVAL)
}
const stopPeriodicCheck = () => {
if (checkInterval) {
clearInterval(checkInterval)
checkInterval = null
}
}
// Check for updates on mount and start periodic checking
onMounted(() => {
checkForUpdate()
startPeriodicCheck()
})
onUnmounted(() => {
stopStatusPolling()
stopPeriodicCheck()
})
return {
updateInfo,
updateStatus,
isChecking,
isUpdating,
checkForUpdate,
applyUpdate
}
}
+1 -1
View File
@@ -1 +1 @@
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}
{"root":["./src/main.ts","./src/vite-env.d.ts","./src/components/index.ts","./src/composables/index.ts","./src/composables/usesse.ts","./src/composables/usestats.ts","./src/composables/usetimeline.ts","./src/composables/useupdate.ts","./src/types/api.ts","./src/types/index.ts","./src/types/observation.ts","./src/types/prompt.ts","./src/types/summary.ts","./src/utils/api.ts","./src/utils/formatters.ts","./src/app.vue","./src/components/badge.vue","./src/components/card.vue","./src/components/filtertabs.vue","./src/components/header.vue","./src/components/iconbox.vue","./src/components/observationcard.vue","./src/components/projectfilter.vue","./src/components/promptcard.vue","./src/components/sidebar.vue","./src/components/statscards.vue","./src/components/summarycard.vue","./src/components/timeline.vue"],"version":"5.7.3"}