Convert botfights from external link to real container app on port 9100. Add manifest, update marketplace/discover/kiosk/session configs, switch registry URLs to git.tx1138.com. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
285 lines
7.3 KiB
Vue
285 lines
7.3 KiB
Vue
<template>
|
|
<div class="kiosk-root" tabindex="0" ref="kioskRoot">
|
|
<!-- Kiosk launcher grid -->
|
|
<div class="kiosk-launcher">
|
|
<!-- Header -->
|
|
<div class="kiosk-header">
|
|
<div class="flex items-center gap-4">
|
|
<img :src="FALLBACK_ICON" alt="Archipelago" class="w-10 h-10" />
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-white font-archipelago">Archipelago</h1>
|
|
<p class="text-sm text-white/50">{{ currentTime }}</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-3">
|
|
<div class="kiosk-status-pill" :class="isConnected ? 'status-success' : 'status-error'">
|
|
<div class="w-2 h-2 rounded-full" :class="isConnected ? 'bg-green-400' : 'bg-red-400'"></div>
|
|
{{ isConnected ? t('kiosk.online') : t('kiosk.offline') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- App grid -->
|
|
<div class="kiosk-grid">
|
|
<button
|
|
v-for="app in launchableApps"
|
|
:key="app.id"
|
|
class="kiosk-app-tile"
|
|
@click="openApp(app)"
|
|
:data-controller-focusable="true"
|
|
>
|
|
<div class="kiosk-app-icon-wrap">
|
|
<img
|
|
:src="app.icon"
|
|
:alt="app.title"
|
|
class="kiosk-app-icon"
|
|
@error="($event.target as HTMLImageElement).src = FALLBACK_ICON"
|
|
/>
|
|
<div
|
|
class="kiosk-app-status"
|
|
:class="app.running ? 'bg-green-400' : 'bg-white/30'"
|
|
/>
|
|
</div>
|
|
<span class="kiosk-app-label">{{ app.title }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="kiosk-footer">
|
|
<span class="text-white/30 text-sm">{{ t('kiosk.navHint') }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useAppStore } from '@/stores/app'
|
|
import { useAppLauncherStore } from '@/stores/appLauncher'
|
|
|
|
const { t } = useI18n()
|
|
const store = useAppStore()
|
|
const appLauncher = useAppLauncherStore()
|
|
const kioskRoot = ref<HTMLElement | null>(null)
|
|
|
|
interface KioskApp {
|
|
id: string
|
|
title: string
|
|
icon: string
|
|
url: string
|
|
running: boolean
|
|
}
|
|
|
|
// Public asset path — construct with BASE_URL to avoid Vite resolving it as a module import
|
|
const FALLBACK_ICON = `${import.meta.env.BASE_URL}assets/img/favico.png`
|
|
|
|
const currentTime = ref('')
|
|
|
|
const isConnected = computed(() => store.isConnected)
|
|
|
|
// Build list of launchable apps from the store's package data
|
|
const launchableApps = computed<KioskApp[]>(() => {
|
|
const pkgs = store.data?.['package-data'] || {}
|
|
const apps: KioskApp[] = []
|
|
|
|
// App URL mappings — use nginx proxy paths for local apps
|
|
const urlMap: Record<string, string> = {
|
|
'bitcoin-knots': '/app/bitcoin-ui/',
|
|
'lnd': '/app/lnd/',
|
|
'mempool': '/app/mempool/',
|
|
'btcpay-server': '/app/btcpay/',
|
|
'homeassistant': '/app/homeassistant/',
|
|
'grafana': '/app/grafana/',
|
|
'jellyfin': '/app/jellyfin/',
|
|
'nextcloud': '/app/nextcloud/',
|
|
'immich': '/app/immich/',
|
|
'photoprism': '/app/photoprism/',
|
|
'vaultwarden': '/app/vaultwarden/',
|
|
'filebrowser': '/app/filebrowser/',
|
|
'searxng': '/app/searxng/',
|
|
'ollama': '/app/ollama/',
|
|
'penpot': '/app/penpot/',
|
|
'onlyoffice': '/app/onlyoffice/',
|
|
'portainer': '/app/portainer/',
|
|
'uptime-kuma': '/app/uptime-kuma/',
|
|
'nginx-proxy-manager': '/app/nginx-proxy-manager/',
|
|
'tailscale': '/app/tailscale/',
|
|
'fedimint': '/app/fedimint/',
|
|
'fedimint-gateway': '/app/fedimint-gateway/',
|
|
'dwn': '/app/dwn/',
|
|
'nostr-rs-relay': '/app/nostr-rs-relay/',
|
|
'indeedhub': 'http://localhost:8190',
|
|
'botfights': 'http://localhost:9100',
|
|
'nwnn': 'https://nwnn.l484.com',
|
|
'484-kitchen': 'https://484.kitchen',
|
|
'call-the-operator': 'https://cta.tx1138.com',
|
|
'arch-presentation': 'https://present.l484.com',
|
|
'syntropy-institute': 'https://syntropy.institute',
|
|
't-zero': 'https://teeminuszero.net',
|
|
}
|
|
|
|
for (const [id, pkg] of Object.entries(pkgs)) {
|
|
const url = urlMap[id]
|
|
if (!url) continue
|
|
|
|
const isRunning = pkg.state === 'running' ||
|
|
pkg.installed?.status === 'running'
|
|
|
|
apps.push({
|
|
id,
|
|
title: pkg.manifest?.title || id,
|
|
icon: pkg['static-files']?.icon || FALLBACK_ICON,
|
|
url,
|
|
running: isRunning,
|
|
})
|
|
}
|
|
|
|
// Sort: running apps first, then alphabetical
|
|
return apps.sort((a, b) => {
|
|
if (a.running !== b.running) return a.running ? -1 : 1
|
|
return a.title.localeCompare(b.title)
|
|
})
|
|
})
|
|
|
|
function openApp(app: KioskApp) {
|
|
// Delegate to the app launcher — handles iframe overlay vs new-tab
|
|
appLauncher.open({ url: app.url, title: app.title })
|
|
}
|
|
|
|
// Clock updater
|
|
let clockInterval: ReturnType<typeof setInterval> | undefined
|
|
function updateClock() {
|
|
const now = new Date()
|
|
currentTime.value = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
onMounted(() => {
|
|
updateClock()
|
|
clockInterval = setInterval(updateClock, 30000)
|
|
kioskRoot.value?.focus()
|
|
|
|
// Connect WebSocket if not already
|
|
if (!store.isConnected) {
|
|
store.connectWebSocket().catch(() => {})
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (clockInterval) clearInterval(clockInterval)
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.kiosk-root {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: #000;
|
|
outline: none;
|
|
overflow: hidden;
|
|
z-index: 9999;
|
|
}
|
|
|
|
.kiosk-launcher {
|
|
height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
padding: 2rem 3rem;
|
|
background: linear-gradient(180deg, #0a0a12 0%, #000 100%);
|
|
}
|
|
|
|
.kiosk-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: 2rem;
|
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
|
margin-bottom: 2rem;
|
|
}
|
|
|
|
.kiosk-status-pill {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.75rem;
|
|
border-radius: 9999px;
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.kiosk-grid {
|
|
flex: 1;
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 1.5rem;
|
|
align-content: start;
|
|
overflow-y: auto;
|
|
padding: 0.5rem;
|
|
}
|
|
|
|
.kiosk-app-tile {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
padding: 1.25rem 0.75rem;
|
|
border-radius: 1rem;
|
|
background: rgba(255, 255, 255, 0.04);
|
|
border: 1px solid rgba(255, 255, 255, 0.06);
|
|
transition: all 0.25s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.kiosk-app-tile:hover,
|
|
.kiosk-app-tile:focus-visible {
|
|
background: rgba(255, 255, 255, 0.1);
|
|
border-color: rgba(251, 146, 60, 0.4);
|
|
transform: scale(1.05);
|
|
box-shadow: 0 0 30px rgba(251, 146, 60, 0.15);
|
|
outline: none;
|
|
}
|
|
|
|
.kiosk-app-icon-wrap {
|
|
position: relative;
|
|
width: 64px;
|
|
height: 64px;
|
|
}
|
|
|
|
.kiosk-app-icon {
|
|
width: 64px;
|
|
height: 64px;
|
|
border-radius: 16px;
|
|
object-fit: cover;
|
|
background: rgba(255, 255, 255, 0.05);
|
|
}
|
|
|
|
.kiosk-app-status {
|
|
position: absolute;
|
|
bottom: -2px;
|
|
right: -2px;
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
border: 3px solid #000;
|
|
}
|
|
|
|
.kiosk-app-label {
|
|
font-size: 0.8125rem;
|
|
font-weight: 500;
|
|
color: rgba(255, 255, 255, 0.85);
|
|
text-align: center;
|
|
line-height: 1.2;
|
|
max-width: 100%;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.kiosk-footer {
|
|
padding-top: 1.5rem;
|
|
text-align: center;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.06);
|
|
margin-top: 1.5rem;
|
|
}
|
|
</style>
|