Update favicon and enhance UI components for improved user experience
- Replaced PNG favicon with SVG for better scalability and visual quality across devices. - Updated Vite configuration to include the new SVG favicon and adjusted asset paths. - Enhanced various UI components with improved focus management and accessibility features. - Introduced new styles to hide scrollbars while maintaining scroll functionality for a cleaner interface.
This commit is contained in:
parent
8a32e36d85
commit
b63612c5ae
@ -2,16 +2,16 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="96x96" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/assets/img/favico.png" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/assets/img/favico.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="96x96" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="128x128" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="192x192" href="/assets/icon/favico-black.svg" />
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="/assets/icon/favico-black.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||
<meta name="description" content="Archipelago - Your sovereign personal server" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
@ -21,7 +21,7 @@
|
||||
<meta name="apple-mobile-web-app-title" content="Archipelago" />
|
||||
<meta name="application-name" content="Archipelago" />
|
||||
<meta name="msapplication-TileColor" content="#000000" />
|
||||
<meta name="msapplication-TileImage" content="/assets/img/favico.png" />
|
||||
<meta name="msapplication-TileImage" content="/assets/icon/favico-black.svg" />
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<title>Archipelago OS</title>
|
||||
</head>
|
||||
|
||||
6
neode-ui/public/assets/img/icons/bitcoin-symbol.svg
Normal file
6
neode-ui/public/assets/img/icons/bitcoin-symbol.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" fill="none">
|
||||
<!-- White B symbol only - no orange circle -->
|
||||
<g transform="scale(0.015625)">
|
||||
<path fill="#ffffff" d="m 46.1009,27.441 c 0.637,-4.258 -2.605,-6.547 -7.038,-8.074 l 1.438,-5.768 -3.511,-0.875 -1.4,5.616 c -0.923,-0.23 -1.871,-0.447 -2.813,-0.662 l 1.41,-5.653 -3.509,-0.875 -1.439,5.766 c -0.764,-0.174 -1.514,-0.346 -2.242,-0.527 l 0.004,-0.018 -4.842,-1.209 -0.934,3.75 c 0,0 2.605,0.597 2.55,0.634 1.422,0.355 1.679,1.296 1.636,2.042 l -1.638,6.571 c 0.098,0.025 0.225,0.061 0.365,0.117 -0.117,-0.029 -0.242,-0.061 -0.371,-0.092 l -2.296,9.205 c -0.174,0.432 -0.615,1.08 -1.609,0.834 0.035,0.051 -2.552,-0.637 -2.552,-0.637 l -1.743,4.019 4.569,1.139 c 0.85,0.213 1.683,0.436 2.503,0.646 l -1.453,5.834 3.507,0.875 1.439,-5.772 c 0.958,0.26 1.888,0.5 2.798,0.726 l -1.434,5.745 3.511,0.875 1.453,-5.823 c 5.987,1.133 10.489,0.676 12.384,-4.739 1.527,-4.36 -0.076,-6.875 -3.226,-8.515 2.294,-0.529 4.022,-2.038 4.483,-5.155 z m -8.022,11.249 c -1.085,4.36 -8.426,2.003 -10.806,1.412 l 1.928,-7.729 c 2.38,0.594 10.012,1.77 8.878,6.317 z m 1.086,-11.312 c -0.99,3.966 -7.1,1.951 -9.082,1.457 l 1.748,-7.01 c 1.982,0.494 8.365,1.416 7.334,5.553 z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -100,9 +100,22 @@ function onUserActivity() {
|
||||
function onKeyDown(e: KeyboardEvent) {
|
||||
const isMac = navigator.platform.toUpperCase().includes('MAC')
|
||||
const mod = isMac ? e.metaKey : e.ctrlKey
|
||||
if (mod && e.key === 'k') {
|
||||
// Cmd+K / Ctrl+K or plain K (when not typing in input)
|
||||
const target = e.target as HTMLElement
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
||||
if ((mod && e.key === 'k') || ((e.key === 'k' || e.key === 'K') && !isInput)) {
|
||||
e.preventDefault()
|
||||
spotlightStore.toggle()
|
||||
return
|
||||
}
|
||||
// 's' key activates screensaver when authenticated (skip if typing in input)
|
||||
if (e.key === 's' || e.key === 'S') {
|
||||
const target = e.target as HTMLElement
|
||||
const isInput = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable
|
||||
if (!isInput && appStore.isAuthenticated && !screensaverStore.isActive) {
|
||||
e.preventDefault()
|
||||
screensaverStore.activate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,14 @@
|
||||
<template>
|
||||
<div class="logo-gradient-border flex-shrink-0 inline-block overflow-hidden" :class="sizeClass">
|
||||
<div
|
||||
class="flex-shrink-0 inline-block overflow-hidden"
|
||||
:class="[
|
||||
sizeClass,
|
||||
!noBorder && 'logo-gradient-border'
|
||||
]"
|
||||
>
|
||||
<!-- Neode logo - always white -->
|
||||
<svg
|
||||
class="block logo-svg"
|
||||
class="block w-full h-full logo-svg"
|
||||
viewBox="0 0 1024 1024"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@ -27,9 +33,18 @@
|
||||
<script setup lang="ts">
|
||||
const props = withDefaults(defineProps<{
|
||||
size?: 'sm' | 'lg' | 'xl'
|
||||
}>(), { size: 'sm' })
|
||||
noBorder?: boolean
|
||||
/** When true, fit to container (w-full h-full) instead of fixed size - for use inside logo-gradient-border */
|
||||
fit?: boolean
|
||||
}>(), { size: 'sm', noBorder: false, fit: false })
|
||||
|
||||
const sizeClass = props.size === 'xl' ? 'w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80' : props.size === 'lg' ? 'w-32 h-32 sm:w-48 sm:h-48' : 'w-14 h-14'
|
||||
const sizeClass = props.fit
|
||||
? 'w-full h-full max-w-full max-h-full'
|
||||
: props.size === 'xl'
|
||||
? 'w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80'
|
||||
: props.size === 'lg'
|
||||
? 'w-32 h-32 sm:w-48 sm:h-48'
|
||||
: 'w-14 h-14'
|
||||
|
||||
// Parsed from favico-black.svg path - 20 rects
|
||||
const rects = [
|
||||
@ -56,7 +71,6 @@ const rects = [
|
||||
]
|
||||
|
||||
// Stagger delays (ms) - row-by-row top-to-bottom, left-to-right for a clean reveal
|
||||
// Row 1 (y~318): 0,1,2,3 | Row 2 (y~396): 4,5 | Row 3 (y~476): 6-11 | Row 4 (y~555): 12-15 | Row 5 (y~634): 16-19
|
||||
const delays = [0, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1100, 1200, 1300, 1400, 1500, 1600, 1700, 1800, 1900]
|
||||
</script>
|
||||
|
||||
|
||||
@ -36,13 +36,15 @@
|
||||
<kbd class="hidden sm:inline-flex px-2 py-1 text-xs text-white/50 bg-white/10 rounded">Esc</kbd>
|
||||
</div>
|
||||
|
||||
<!-- Iframe container -->
|
||||
<div class="relative flex-1 min-h-0 bg-black/40">
|
||||
<!-- Iframe container - overflow hidden to clip inner scrollbars -->
|
||||
<div class="relative flex-1 min-h-0 bg-black/40 overflow-hidden">
|
||||
<iframe
|
||||
ref="iframeRef"
|
||||
v-if="store.url"
|
||||
:src="store.url"
|
||||
class="absolute inset-0 w-full h-full border-0"
|
||||
class="absolute inset-0 w-full h-full border-0 iframe-scrollbar-hide"
|
||||
title="App content"
|
||||
@load="injectScrollbarHideIfSameOrigin"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -57,6 +59,22 @@ import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
|
||||
const store = useAppLauncherStore()
|
||||
const closeBtnRef = ref<HTMLButtonElement | null>(null)
|
||||
const iframeRef = ref<HTMLIFrameElement | null>(null)
|
||||
|
||||
function injectScrollbarHideIfSameOrigin() {
|
||||
try {
|
||||
const doc = iframeRef.value?.contentDocument
|
||||
if (!doc) return
|
||||
const style = doc.createElement('style')
|
||||
style.textContent = `
|
||||
* { -ms-overflow-style: none; scrollbar-width: none; }
|
||||
*::-webkit-scrollbar { display: none; }
|
||||
`
|
||||
doc.head.appendChild(style)
|
||||
} catch {
|
||||
/* Cross-origin: cannot access iframe document */
|
||||
}
|
||||
}
|
||||
|
||||
const panelClasses = [
|
||||
'glass-card',
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="modalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 max-w-lg w-full relative z-10 max-h-[80vh] overflow-y-auto"
|
||||
>
|
||||
@ -45,14 +46,20 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
import { ref, computed } from 'vue'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const props = defineProps<{
|
||||
show: boolean
|
||||
title: string
|
||||
content: string
|
||||
relatedPath?: string
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const modalRef = ref<HTMLElement | null>(null)
|
||||
useModalKeyboard(modalRef, computed(() => props.show), () => emit('close'))
|
||||
</script>
|
||||
|
||||
@ -1,39 +1,59 @@
|
||||
<template>
|
||||
<Transition name="fade">
|
||||
<div
|
||||
v-if="showUpdatePrompt"
|
||||
class="fixed bottom-4 left-4 right-4 md:left-auto md:right-4 md:w-96 z-[9999]"
|
||||
>
|
||||
<div class="glass-card p-4 flex items-center gap-4">
|
||||
<div class="flex-1">
|
||||
<p class="text-white/90 text-sm font-medium mb-1">Update Available</p>
|
||||
<p class="text-white/70 text-xs">A new version is available. Click to update.</p>
|
||||
<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 gradient-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Update Now
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
@click="handleUpdate"
|
||||
class="glass-button px-4 py-2 rounded-lg text-sm font-medium transition-all hover:bg-black/70 hover:border-white/30"
|
||||
>
|
||||
Update
|
||||
</button>
|
||||
<button
|
||||
@click="dismissUpdate"
|
||||
class="text-white/50 hover:text-white/80 transition-colors p-2"
|
||||
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>
|
||||
</div>
|
||||
</Transition>
|
||||
</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
|
||||
@ -54,6 +74,13 @@ onMounted(() => {
|
||||
// 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) {
|
||||
@ -79,6 +106,8 @@ onMounted(() => {
|
||||
}
|
||||
})
|
||||
|
||||
useModalKeyboard(modalRef, showUpdatePrompt, dismissUpdate)
|
||||
|
||||
function dismissUpdate() {
|
||||
showUpdatePrompt.value = false
|
||||
}
|
||||
@ -89,17 +118,3 @@ async function handleUpdate() {
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@ -19,8 +19,8 @@
|
||||
/>
|
||||
</div>
|
||||
<!-- Logo in center -->
|
||||
<div class="screensaver-logo-wrapper relative z-10">
|
||||
<AnimatedLogo size="xl" />
|
||||
<div class="screensaver-logo-wrapper">
|
||||
<ScreensaverLogo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -30,7 +30,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onBeforeUnmount } from 'vue'
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
import ScreensaverLogo from '@/components/ScreensaverLogo.vue'
|
||||
import { useScreensaverStore } from '@/stores/screensaver'
|
||||
|
||||
const store = useScreensaverStore()
|
||||
@ -100,12 +100,14 @@ onBeforeUnmount(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Ring of segments around the logo - audio viz style */
|
||||
/* Ring of segments around the logo - audio viz style (behind logo) */
|
||||
.screensaver-viz-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
--viz-radius: 140px;
|
||||
}
|
||||
|
||||
@ -175,6 +177,7 @@ onBeforeUnmount(() => {
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
filter: drop-shadow(0 0 40px rgba(255, 255, 255, 0.15));
|
||||
}
|
||||
</style>
|
||||
|
||||
20
neode-ui/src/components/ScreensaverLogo.vue
Normal file
20
neode-ui/src/components/ScreensaverLogo.vue
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div class="logo-gradient-border screensaver-logo-cycle relative w-48 h-48 sm:w-64 sm:h-64 md:w-80 md:h-80 flex items-center justify-center overflow-hidden">
|
||||
<!-- Squares logo -->
|
||||
<div class="screensaver-logo-squares absolute inset-[3px] flex items-center justify-center">
|
||||
<AnimatedLogo size="xl" no-border fit />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import AnimatedLogo from '@/components/AnimatedLogo.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.screensaver-logo-squares {
|
||||
opacity: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
</style>
|
||||
@ -218,6 +218,16 @@ export function useControllerNav(containerRef?: { value: HTMLElement | null }) {
|
||||
return
|
||||
}
|
||||
}
|
||||
// My Apps: Enter = launch (click Launch button when app is runnable)
|
||||
if (el.hasAttribute('data-controller-launch')) {
|
||||
const launchBtn = el.querySelector<HTMLButtonElement>('[data-controller-launch-btn]:not([disabled])')
|
||||
if (launchBtn) {
|
||||
playNavSound('action')
|
||||
launchBtn.click()
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
}
|
||||
// My Apps, etc: Enter = focus first inner control
|
||||
const inner = getInnerFocusables(el)
|
||||
const firstInner = inner[0]
|
||||
|
||||
74
neode-ui/src/composables/useModalKeyboard.ts
Normal file
74
neode-ui/src/composables/useModalKeyboard.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Modal keyboard navigation: Escape to close, Arrow keys to move between buttons.
|
||||
* Restores focus to the previously active element when closing via Escape.
|
||||
*/
|
||||
|
||||
import { onMounted, onBeforeUnmount, watch, type Ref } from 'vue'
|
||||
|
||||
const FOCUSABLE = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||
|
||||
export interface UseModalKeyboardOptions {
|
||||
restoreFocusRef?: Ref<HTMLElement | null>
|
||||
}
|
||||
|
||||
export function useModalKeyboard(
|
||||
containerRef: Ref<HTMLElement | null>,
|
||||
isOpen: Ref<boolean>,
|
||||
onClose: () => void,
|
||||
options?: UseModalKeyboardOptions
|
||||
) {
|
||||
const restoreFocusRef = options?.restoreFocusRef
|
||||
|
||||
// Save the element that had focus when modal opens (before focus moves to modal)
|
||||
watch(isOpen, (open) => {
|
||||
if (open && restoreFocusRef) {
|
||||
restoreFocusRef.value = document.activeElement as HTMLElement | null
|
||||
}
|
||||
})
|
||||
function getFocusables(): HTMLElement[] {
|
||||
const el = containerRef.value
|
||||
if (!el) return []
|
||||
return Array.from(el.querySelectorAll<HTMLElement>(FOCUSABLE)).filter(
|
||||
(e) => e.offsetParent !== null
|
||||
)
|
||||
}
|
||||
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (!isOpen.value) return
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
restoreFocusRef?.value?.focus?.()
|
||||
onClose()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
|
||||
const focusables = getFocusables()
|
||||
if (focusables.length === 0) return
|
||||
|
||||
const current = document.activeElement as HTMLElement | null
|
||||
const idx = current ? focusables.indexOf(current) : -1
|
||||
|
||||
let nextIdx: number
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
|
||||
nextIdx = idx < focusables.length - 1 ? idx + 1 : 0
|
||||
} else {
|
||||
nextIdx = idx > 0 ? idx - 1 : focusables.length - 1
|
||||
}
|
||||
|
||||
focusables[nextIdx]?.focus()
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('keydown', handleKeydown, true)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('keydown', handleKeydown, true)
|
||||
})
|
||||
}
|
||||
@ -70,8 +70,10 @@ export const useSpotlightStore = defineStore('spotlight', () => {
|
||||
content: '',
|
||||
relatedPath: undefined as string | undefined,
|
||||
})
|
||||
const helpModalRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function showHelpModal(payload: { title: string; content: string; relatedPath?: string }) {
|
||||
helpModalRestoreFocusRef.value = document.activeElement as HTMLElement | null
|
||||
helpModal.show = true
|
||||
helpModal.title = payload.title
|
||||
helpModal.content = payload.content
|
||||
@ -79,6 +81,8 @@ export const useSpotlightStore = defineStore('spotlight', () => {
|
||||
}
|
||||
|
||||
function closeHelpModal() {
|
||||
helpModalRestoreFocusRef.value?.focus?.()
|
||||
helpModalRestoreFocusRef.value = null
|
||||
helpModal.show = false
|
||||
}
|
||||
|
||||
|
||||
@ -444,6 +444,28 @@ body {
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.4) 0%, rgba(255, 255, 255, 0.2) 100%);
|
||||
}
|
||||
|
||||
/* Hide scrollbar but keep scroll functionality - applied globally to all scrollable content */
|
||||
.scrollbar-hide,
|
||||
.overflow-y-auto,
|
||||
.overflow-auto,
|
||||
.overflow-y-scroll,
|
||||
.iframe-scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar,
|
||||
.overflow-y-auto::-webkit-scrollbar,
|
||||
.overflow-auto::-webkit-scrollbar,
|
||||
.overflow-y-scroll::-webkit-scrollbar,
|
||||
.iframe-scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Iframe scrollbar hide - targets iframe element; inner doc scrollbars need same-origin injection */
|
||||
iframe.iframe-scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes fadeUpIn {
|
||||
0% {
|
||||
|
||||
@ -383,10 +383,11 @@
|
||||
<div
|
||||
v-if="uninstallModal.show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 md:p-8 max-w-md w-full relative z-10"
|
||||
>
|
||||
@ -407,7 +408,7 @@
|
||||
|
||||
<div class="flex flex-col-reverse md:flex-row gap-3 md:justify-end">
|
||||
<button
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
class="w-full md:w-auto px-6 py-3 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
@ -432,6 +433,7 @@ import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState } from '../types/api'
|
||||
import { useMobileBackButton } from '../composables/useMobileBackButton'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
import { dummyApps } from '../utils/dummyApps'
|
||||
|
||||
const { bottomPosition } = useMobileBackButton()
|
||||
@ -510,6 +512,18 @@ const uninstallModal = ref({
|
||||
show: false,
|
||||
appTitle: ''
|
||||
})
|
||||
const uninstallModalRef = ref<HTMLElement | null>(null)
|
||||
const uninstallRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeUninstallModal() {
|
||||
uninstallRestoreFocusRef.value?.focus?.()
|
||||
uninstallModal.value.show = false
|
||||
}
|
||||
useModalKeyboard(
|
||||
uninstallModalRef,
|
||||
computed(() => uninstallModal.value.show),
|
||||
closeUninstallModal,
|
||||
{ restoreFocusRef: uninstallRestoreFocusRef }
|
||||
)
|
||||
|
||||
// Determine back button text based on where user came from
|
||||
const backButtonText = computed(() => {
|
||||
@ -607,6 +621,10 @@ function launchApp() {
|
||||
dev: 'http://localhost:8103',
|
||||
prod: 'http://localhost:8103' // Self-hosted splash screen
|
||||
},
|
||||
'indeedhub': {
|
||||
dev: 'http://localhost:7777',
|
||||
prod: 'http://localhost:7777' // Containerized indeehub prototype
|
||||
},
|
||||
// Dummy apps - replace with real URLs when packaged
|
||||
'bitcoin': {
|
||||
dev: 'http://localhost:8332',
|
||||
@ -663,7 +681,11 @@ function launchApp() {
|
||||
}
|
||||
|
||||
if (appUrls[id]) {
|
||||
const url = isDev ? appUrls[id].dev : appUrls[id].prod
|
||||
let url = isDev ? appUrls[id].dev : appUrls[id].prod
|
||||
// Replace localhost with current hostname for remote access
|
||||
if (url.includes('localhost')) {
|
||||
url = url.replace('localhost', window.location.hostname)
|
||||
}
|
||||
useAppLauncherStore().open({ url, title: pkg.value.manifest.title })
|
||||
return
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
v-for="[id, pkg] in sortedPackageEntries"
|
||||
:key="id"
|
||||
data-controller-container
|
||||
:data-controller-launch="canLaunch(pkg) ? '' : undefined"
|
||||
tabindex="0"
|
||||
class="glass-card p-6 transition-all hover:-translate-y-1 cursor-pointer relative min-w-0 overflow-hidden"
|
||||
@click="goToApp(id as string)"
|
||||
@ -75,6 +76,7 @@
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button
|
||||
v-if="canLaunch(pkg)"
|
||||
data-controller-launch-btn
|
||||
@click.stop="launchApp(id as string)"
|
||||
class="flex-1 px-4 py-2 gradient-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
@ -125,10 +127,11 @@
|
||||
<div
|
||||
v-if="uninstallModal.show"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4"
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
>
|
||||
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm"></div>
|
||||
<div
|
||||
ref="uninstallModalRef"
|
||||
@click.stop
|
||||
class="glass-card p-6 max-w-md w-full relative z-10"
|
||||
>
|
||||
@ -149,7 +152,7 @@
|
||||
|
||||
<div class="flex gap-3 justify-end">
|
||||
<button
|
||||
@click="uninstallModal.show = false"
|
||||
@click="closeUninstallModal()"
|
||||
class="px-4 py-2 glass-button rounded-lg text-sm font-medium"
|
||||
>
|
||||
Cancel
|
||||
@ -173,6 +176,7 @@ import { useRouter, RouterLink } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import { useAppLauncherStore } from '../stores/appLauncher'
|
||||
import { PackageState } from '../types/api'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@ -200,6 +204,18 @@ const uninstallModal = ref({
|
||||
appId: '',
|
||||
appTitle: ''
|
||||
})
|
||||
const uninstallModalRef = ref<HTMLElement | null>(null)
|
||||
const uninstallRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeUninstallModal() {
|
||||
uninstallRestoreFocusRef.value?.focus?.()
|
||||
uninstallModal.value.show = false
|
||||
}
|
||||
useModalKeyboard(
|
||||
uninstallModalRef,
|
||||
computed(() => uninstallModal.value.show),
|
||||
closeUninstallModal,
|
||||
{ restoreFocusRef: uninstallRestoreFocusRef }
|
||||
)
|
||||
|
||||
function canLaunch(pkg: any): boolean {
|
||||
// For dummy apps, allow launch if running (they have interface addresses)
|
||||
@ -237,6 +253,10 @@ function launchApp(id: string) {
|
||||
'k484': {
|
||||
dev: 'http://localhost:8103',
|
||||
prod: 'http://localhost:8103' // Self-hosted splash screen
|
||||
},
|
||||
'indeedhub': {
|
||||
dev: 'http://localhost:7777',
|
||||
prod: 'http://localhost:7777' // Containerized indeehub prototype
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -218,12 +218,12 @@
|
||||
<div
|
||||
v-if="showSideloadModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showSideloadModal = false"
|
||||
@click.self="closeSideloadModal()"
|
||||
>
|
||||
<div class="glass-card p-8 max-w-2xl w-full relative">
|
||||
<div ref="sideloadModalRef" class="glass-card p-8 max-w-2xl w-full relative">
|
||||
<!-- Close Button -->
|
||||
<button
|
||||
@click="showSideloadModal = false"
|
||||
@click="closeSideloadModal()"
|
||||
class="absolute top-4 right-4 text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -289,14 +289,14 @@
|
||||
<div
|
||||
v-if="showFilterModal"
|
||||
class="fixed inset-0 z-50 flex items-end justify-center md:hidden bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showFilterModal = false"
|
||||
@click.self="closeFilterModal()"
|
||||
>
|
||||
<div class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
<div ref="filterModalRef" class="glass-card p-6 w-full rounded-t-3xl max-h-[80vh] overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold text-white">Filter by Category</h2>
|
||||
<button
|
||||
@click="showFilterModal = false"
|
||||
@click="closeFilterModal()"
|
||||
class="text-white/60 hover:text-white transition-colors"
|
||||
>
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
@ -310,7 +310,7 @@
|
||||
<button
|
||||
v-for="category in categoriesWithApps"
|
||||
:key="category.id"
|
||||
@click="selectedCategory = category.id; showFilterModal = false"
|
||||
@click="selectedCategory = category.id; closeFilterModal()"
|
||||
:class="[
|
||||
'p-4 rounded-xl font-medium transition-all text-left',
|
||||
selectedCategory === category.id
|
||||
@ -372,6 +372,7 @@ import { useAppStore } from '@/stores/app'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMarketplaceApp } from '@/composables/useMarketplaceApp'
|
||||
import { useMobileBackButton } from '@/composables/useMobileBackButton'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@ -408,6 +409,13 @@ const maxAttempts = ref(60)
|
||||
|
||||
// Sideload modal state
|
||||
const showSideloadModal = ref(false)
|
||||
const sideloadModalRef = ref<HTMLElement | null>(null)
|
||||
const sideloadRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeSideloadModal() {
|
||||
sideloadRestoreFocusRef.value?.focus?.()
|
||||
showSideloadModal.value = false
|
||||
}
|
||||
useModalKeyboard(sideloadModalRef, showSideloadModal, closeSideloadModal, { restoreFocusRef: sideloadRestoreFocusRef })
|
||||
const sideloadUrl = ref('')
|
||||
const sideloading = ref(false)
|
||||
const sideloadError = ref('')
|
||||
@ -415,6 +423,13 @@ const sideloadSuccess = ref('')
|
||||
|
||||
// Filter modal state (for mobile)
|
||||
const showFilterModal = ref(false)
|
||||
const filterModalRef = ref<HTMLElement | null>(null)
|
||||
const filterRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeFilterModal() {
|
||||
filterRestoreFocusRef.value?.focus?.()
|
||||
showFilterModal.value = false
|
||||
}
|
||||
useModalKeyboard(filterModalRef, showFilterModal, closeFilterModal, { restoreFocusRef: filterRestoreFocusRef })
|
||||
|
||||
// Community marketplace state
|
||||
const loadingCommunity = ref(false)
|
||||
|
||||
@ -108,9 +108,9 @@
|
||||
<div
|
||||
v-if="showChangePasswordModal"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
|
||||
@click.self="showChangePasswordModal = false"
|
||||
@click.self="closeChangePasswordModal()"
|
||||
>
|
||||
<div class="glass-card p-6 max-w-md w-full">
|
||||
<div ref="changePasswordModalRef" class="glass-card p-6 max-w-md w-full">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Change Password</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Updates both web login and SSH access. Use a strong password (12+ chars, upper, lower, digit, special).</p>
|
||||
<form @submit.prevent="handleChangePassword" class="space-y-4">
|
||||
@ -206,6 +206,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useAppStore } from '../stores/app'
|
||||
import ControllerIndicator from '@/components/ControllerIndicator.vue'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useAppStore()
|
||||
@ -225,6 +226,9 @@ const userDid = computed(() => {
|
||||
|
||||
const copiedOnion = ref(false)
|
||||
const showChangePasswordModal = ref(false)
|
||||
const changePasswordModalRef = ref<HTMLElement | null>(null)
|
||||
const changePasswordRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
useModalKeyboard(changePasswordModalRef, showChangePasswordModal, closeChangePasswordModal, { restoreFocusRef: changePasswordRestoreFocusRef })
|
||||
const changingPassword = ref(false)
|
||||
const changePasswordError = ref('')
|
||||
const changePasswordSuccess = ref('')
|
||||
@ -301,6 +305,7 @@ async function copyOnionAddress() {
|
||||
}
|
||||
|
||||
function closeChangePasswordModal() {
|
||||
changePasswordRestoreFocusRef.value?.focus?.()
|
||||
showChangePasswordModal.value = false
|
||||
changePasswordError.value = ''
|
||||
changePasswordSuccess.value = ''
|
||||
|
||||
@ -108,8 +108,8 @@
|
||||
|
||||
<!-- Send Message Modal -->
|
||||
<Teleport to="body">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="showSendMessageModal = false">
|
||||
<div class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<div v-if="showSendMessageModal" class="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm" @click.self="closeSendMessageModal()">
|
||||
<div ref="sendMessageModalRef" class="glass-card p-6 max-w-md w-full max-h-[90vh] overflow-y-auto">
|
||||
<h3 class="text-lg font-semibold text-white mb-4">Send Message (over Tor)</h3>
|
||||
<p class="text-white/70 text-sm mb-4">Messages are sent over the Tor network to the selected peer.</p>
|
||||
<div class="space-y-4">
|
||||
@ -144,7 +144,7 @@
|
||||
{{ sendingMessage ? 'Sending...' : 'Send' }}
|
||||
</button>
|
||||
<button
|
||||
@click="showSendMessageModal = false"
|
||||
@click="closeSendMessageModal()"
|
||||
class="px-4 py-2 rounded-lg bg-white/10 text-white font-medium hover:bg-white/20 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
@ -630,6 +630,7 @@ import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { rpcClient } from '@/api/rpc-client'
|
||||
import { useMessageToast } from '@/composables/useMessageToast'
|
||||
import { useModalKeyboard } from '@/composables/useModalKeyboard'
|
||||
|
||||
const route = useRoute()
|
||||
const messageToast = useMessageToast()
|
||||
@ -666,6 +667,13 @@ const connectedNodesCount = computed(() => peers.value.length)
|
||||
|
||||
// Send Message modal
|
||||
const showSendMessageModal = ref(false)
|
||||
const sendMessageModalRef = ref<HTMLElement | null>(null)
|
||||
const sendMessageRestoreFocusRef = ref<HTMLElement | null>(null)
|
||||
function closeSendMessageModal() {
|
||||
sendMessageRestoreFocusRef.value?.focus?.()
|
||||
showSendMessageModal.value = false
|
||||
}
|
||||
useModalKeyboard(sendMessageModalRef, showSendMessageModal, closeSendMessageModal, { restoreFocusRef: sendMessageRestoreFocusRef })
|
||||
const sendMessageTo = ref('')
|
||||
const sendMessageText = ref('')
|
||||
const sendingMessage = ref(false)
|
||||
|
||||
@ -9,7 +9,7 @@ export default defineConfig({
|
||||
vue(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: ['favico.png', 'favico.svg', 'favicon.ico'],
|
||||
includeAssets: ['assets/icon/favico-black.svg', 'favico.svg', 'favicon.ico'],
|
||||
manifest: {
|
||||
name: 'Archipelago',
|
||||
short_name: 'Archipelago',
|
||||
@ -24,63 +24,63 @@ export default defineConfig({
|
||||
prefer_related_applications: false,
|
||||
icons: [
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '72x72',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '96x96',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '128x128',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '144x144',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '152x152',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '384x384',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'any'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'maskable'
|
||||
},
|
||||
{
|
||||
src: '/assets/img/favico.png',
|
||||
src: '/assets/icon/favico-black.svg',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
type: 'image/svg+xml',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
],
|
||||
@ -90,21 +90,21 @@ export default defineConfig({
|
||||
short_name: 'Dashboard',
|
||||
description: 'Open the dashboard',
|
||||
url: '/dashboard',
|
||||
icons: [{ src: '/assets/img/favico.png', sizes: '192x192' }]
|
||||
icons: [{ src: '/assets/icon/favico-black.svg', sizes: '192x192' }]
|
||||
},
|
||||
{
|
||||
name: 'My Apps',
|
||||
short_name: 'Apps',
|
||||
description: 'Manage your apps',
|
||||
url: '/apps',
|
||||
icons: [{ src: '/assets/img/favico.png', sizes: '192x192' }]
|
||||
icons: [{ src: '/assets/icon/favico-black.svg', sizes: '192x192' }]
|
||||
},
|
||||
{
|
||||
name: 'App Store',
|
||||
short_name: 'Store',
|
||||
description: 'Browse and install apps',
|
||||
url: '/marketplace',
|
||||
icons: [{ src: '/assets/img/favico.png', sizes: '192x192' }]
|
||||
icons: [{ src: '/assets/icon/favico-black.svg', sizes: '192x192' }]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -136,6 +136,10 @@ if [ "$LIVE" = true ]; then
|
||||
# Restart services
|
||||
echo " Restarting services..."
|
||||
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo systemctl start archipelago && sudo systemctl restart nginx"
|
||||
|
||||
# Set up HTTPS for PWA installability (browsers require secure context)
|
||||
echo " Setting up HTTPS for PWA install..."
|
||||
sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" "sudo bash $TARGET_DIR/scripts/setup-https-dev.sh" 2>&1 | sed 's/^/ /' || true
|
||||
|
||||
# Rebuild and recreate LND UI container (port 8081 so Launch from UI and http://host:8081 both work)
|
||||
echo " Rebuilding LND UI..."
|
||||
@ -428,6 +432,7 @@ if [ "$LIVE" = true ]; then
|
||||
echo "✅ Deployed to live system!"
|
||||
echo " Backend: $(sshpass -p "$ARCHIPELAGO_PASSWORD" ssh $SSH_OPTS "$TARGET_HOST" 'sudo systemctl is-active archipelago')"
|
||||
echo " Web UI: http://$(echo $TARGET_HOST | cut -d@ -f2)"
|
||||
echo " PWA install: https://$(echo $TARGET_HOST | cut -d@ -f2) (use HTTPS, accept cert once, then Install app)"
|
||||
else
|
||||
echo ""
|
||||
echo "✅ Build complete!"
|
||||
|
||||
98
scripts/setup-https-dev.sh
Normal file
98
scripts/setup-https-dev.sh
Normal file
@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Set up HTTPS on Archipelago dev server for PWA installability.
|
||||
# Browsers require HTTPS (or localhost) to install PWAs.
|
||||
# Generates a self-signed certificate and configures nginx.
|
||||
#
|
||||
# Run on the target server: sudo ./setup-https-dev.sh
|
||||
# Or via deploy: the deploy script runs this automatically.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SSL_DIR="/etc/archipelago/ssl"
|
||||
NGINX_CFG="/etc/nginx/sites-available/archipelago"
|
||||
CERT="$SSL_DIR/archipelago.crt"
|
||||
KEY="$SSL_DIR/archipelago.key"
|
||||
|
||||
# Create SSL directory
|
||||
mkdir -p "$SSL_DIR"
|
||||
chmod 755 "$SSL_DIR"
|
||||
|
||||
# Generate self-signed cert if missing (valid 365 days)
|
||||
# SAN includes common dev IPs so cert works when accessing via IP
|
||||
if [ ! -f "$CERT" ] || [ ! -f "$KEY" ]; then
|
||||
echo "Generating self-signed certificate for PWA (HTTPS)..."
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
|
||||
-keyout "$KEY" \
|
||||
-out "$CERT" \
|
||||
-subj "/CN=archipelago.local/O=Archipelago/C=US" \
|
||||
-addext "subjectAltName=DNS:archipelago.local,DNS:localhost,IP:127.0.0.1,IP:192.168.1.228,IP:192.168.1.198,IP:10.0.0.1"
|
||||
chmod 644 "$CERT"
|
||||
chmod 600 "$KEY"
|
||||
echo " Certificate created at $CERT"
|
||||
fi
|
||||
|
||||
# Check if HTTPS is already configured
|
||||
if grep -q "listen 443 ssl" "$NGINX_CFG" 2>/dev/null; then
|
||||
echo "HTTPS already configured in nginx."
|
||||
nginx -t 2>/dev/null && systemctl reload nginx
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Add HTTPS server block (duplicate of HTTP block with SSL)
|
||||
# Insert after the closing brace of the first server block
|
||||
HTTPS_BLOCK='
|
||||
# HTTPS - required for PWA install (Add to Home Screen) from dev servers
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate '"$CERT"';
|
||||
ssl_certificate_key '"$KEY"';
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
|
||||
root /opt/archipelago/web-ui;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /archipelago/ {
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /rpc/ {
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_connect_timeout 300s;
|
||||
proxy_send_timeout 300s;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
|
||||
location /ws {
|
||||
proxy_pass http://127.0.0.1:5678;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 86400s;
|
||||
}
|
||||
}
|
||||
'
|
||||
|
||||
# Append HTTPS block to nginx config
|
||||
echo "$HTTPS_BLOCK" >> "$NGINX_CFG"
|
||||
echo "Added HTTPS (port 443) to nginx config."
|
||||
|
||||
# Test and reload
|
||||
nginx -t && systemctl reload nginx
|
||||
echo ""
|
||||
echo "HTTPS enabled. Access via https://192.168.1.228 (accept the certificate warning once to install PWA)."
|
||||
Loading…
x
Reference in New Issue
Block a user