mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
Release to the world.
This commit is contained in:
+6
-1
@@ -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 -->
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
export { useSSE } from './useSSE'
|
||||
export { useStats } from './useStats'
|
||||
export { useTimeline } from './useTimeline'
|
||||
export { useUpdate } from './useUpdate'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user