archy/neode-ui/src/components/PWAUpdatePrompt.vue
Dorian 486fc39249 feat: complete Phase 1 foundation hardening + three-mode UI design doc
Phase 1a — Gradient Removal:
- Replaced all gradient-button/gradient-card with glass-button/path-option-card
- Removed banned gradient CSS classes

Phase 1b — Security Hardening:
- SecretsManager: AES-256-GCM encryption (core/security)
- electrs_status: credentials from env vars instead of hardcoded
- port_manager: RwLock proper error handling (no unwrap)
- Pinned all 11 :latest manifest images to specific versions
- parmanode converter: pinned inferred image versions

Phase 1c — Code Quality:
- Split rpc.rs (1795 lines) into 6 handler modules (auth, node, container, package, peers)
- Removed sideload code (UI, store, RPC client, 3 doc files)
- Fixed body background flash on logout/refresh
- Replaced 30 TypeScript `any` types with proper types
- Deleted HelloWorld.vue, removed TODO comments
- Added set -euo pipefail to all shell scripts
- Made deploy script verbose with timestamps and elapsed time

Also adds:
- CLAUDE.md project guide
- docs/three-mode-ui-design.md — design spec for Easy/Pro/Chat UI modes
- OnlineStatusPill component

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 05:23:42 +00:00

121 lines
3.9 KiB
Vue

<template>
<Teleport to="body">
<Transition name="modal">
<div
v-if="showUpdatePrompt"
class="fixed inset-0 z-[9999] flex items-center justify-center p-4"
@click.self="dismissUpdate"
>
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
<div
ref="modalRef"
class="glass-card p-6 max-w-md w-full relative z-10"
@click.stop
>
<div class="flex items-start justify-between gap-4 mb-4">
<h3 class="text-xl font-semibold text-white">Update Available</h3>
<button
@click="dismissUpdate"
class="p-2 rounded-lg hover:bg-white/10 text-white/70 hover:text-white transition-colors"
aria-label="Dismiss"
>
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<p class="text-white/80 mb-6">
A new version of Archipelago is available. Update now to get the latest features and fixes.
</p>
<div class="flex gap-3 justify-end">
<button
@click="dismissUpdate"
class="px-4 py-2 glass-button rounded-lg text-sm font-medium"
>
Later
</button>
<button
@click="handleUpdate"
class="px-4 py-2 glass-button glass-button-sm rounded-lg text-sm font-medium"
>
Update Now
</button>
</div>
</div>
</div>
</Transition>
</Teleport>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useModalKeyboard } from '@/composables/useModalKeyboard'
const showUpdatePrompt = ref(false)
let updateCallback: (() => Promise<void>) | null = null
const modalRef = ref<HTMLElement | null>(null)
onMounted(() => {
// Listen for service worker updates
if ('serviceWorker' in navigator) {
navigator.serviceWorker.addEventListener('controllerchange', () => {
// Service worker updated, reload the page
window.location.reload()
})
// Check for updates periodically
const checkForUpdates = async () => {
const registration = await navigator.serviceWorker.getRegistration()
if (registration) {
await registration.update()
}
}
// Check for updates every 5 minutes
setInterval(checkForUpdates, 5 * 60 * 1000)
// Check when user returns to tab (helps with cached PWA)
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
checkForUpdates()
}
})
// Listen for updatefound event
navigator.serviceWorker.getRegistration().then((registration) => {
if (registration) {
registration.addEventListener('updatefound', () => {
const newWorker = registration.installing
if (newWorker) {
newWorker.addEventListener('statechange', () => {
if (newWorker.state === 'installed' && navigator.serviceWorker.controller) {
// New service worker installed, show update prompt
showUpdatePrompt.value = true
updateCallback = async () => {
if (newWorker.state === 'installed' && registration.waiting) {
// Skip waiting and activate the new service worker
registration.waiting.postMessage({ type: 'SKIP_WAITING' })
}
}
}
})
}
})
}
})
}
})
useModalKeyboard(modalRef, showUpdatePrompt, dismissUpdate)
function dismissUpdate() {
showUpdatePrompt.value = false
}
async function handleUpdate() {
if (updateCallback) {
await updateCallback()
}
}
</script>