Compare commits
3 Commits
6d02f1db3f
...
45b1903e6b
| Author | SHA1 | Date | |
|---|---|---|---|
| 45b1903e6b | |||
| 68274d8651 | |||
| 98a1c1ad97 |
|
|
@ -2,7 +2,6 @@ import type { CSSProperties } from 'hono/jsx'
|
||||||
import {
|
import {
|
||||||
apps,
|
apps,
|
||||||
selectedApp,
|
selectedApp,
|
||||||
setSelectedApp,
|
|
||||||
setSidebarSection,
|
setSidebarSection,
|
||||||
sidebarSection,
|
sidebarSection,
|
||||||
} from '../state'
|
} from '../state'
|
||||||
|
|
@ -17,19 +16,12 @@ import {
|
||||||
interface AppSelectorProps {
|
interface AppSelectorProps {
|
||||||
render: () => void
|
render: () => void
|
||||||
onSelect?: () => void
|
onSelect?: () => void
|
||||||
onDashboard?: () => void
|
|
||||||
collapsed?: boolean
|
collapsed?: boolean
|
||||||
switcherStyle?: CSSProperties
|
switcherStyle?: CSSProperties
|
||||||
listStyle?: CSSProperties
|
listStyle?: CSSProperties
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AppSelector({ render, onSelect, onDashboard, collapsed, switcherStyle, listStyle }: AppSelectorProps) {
|
export function AppSelector({ render, onSelect, collapsed, switcherStyle, listStyle }: AppSelectorProps) {
|
||||||
const selectApp = (name: string) => {
|
|
||||||
setSelectedApp(name)
|
|
||||||
onSelect?.()
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const switchSection = (section: 'apps' | 'tools') => {
|
const switchSection = (section: 'apps' | 'tools') => {
|
||||||
setSidebarSection(section)
|
setSidebarSection(section)
|
||||||
render()
|
render()
|
||||||
|
|
@ -52,9 +44,9 @@ export function AppSelector({ render, onSelect, onDashboard, collapsed, switcher
|
||||||
</SectionSwitcher>
|
</SectionSwitcher>
|
||||||
)}
|
)}
|
||||||
<AppList style={listStyle}>
|
<AppList style={listStyle}>
|
||||||
{collapsed && onDashboard && (
|
{collapsed && (
|
||||||
<AppItem
|
<AppItem
|
||||||
onClick={onDashboard}
|
href="/"
|
||||||
selected={!selectedApp ? true : undefined}
|
selected={!selectedApp ? true : undefined}
|
||||||
style={{ justifyContent: 'center', padding: '10px 12px' }}
|
style={{ justifyContent: 'center', padding: '10px 12px' }}
|
||||||
title="Toes"
|
title="Toes"
|
||||||
|
|
@ -65,7 +57,8 @@ export function AppSelector({ render, onSelect, onDashboard, collapsed, switcher
|
||||||
{activeApps.map(app => (
|
{activeApps.map(app => (
|
||||||
<AppItem
|
<AppItem
|
||||||
key={app.name}
|
key={app.name}
|
||||||
onClick={() => selectApp(app.name)}
|
href={`/app/${app.name}`}
|
||||||
|
onClick={onSelect}
|
||||||
selected={app.name === selectedApp ? true : undefined}
|
selected={app.name === selectedApp ? true : undefined}
|
||||||
style={collapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
|
style={collapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
|
||||||
title={collapsed ? app.name : undefined}
|
title={collapsed ? app.name : undefined}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { useEffect } from 'hono/jsx'
|
import { useEffect } from 'hono/jsx'
|
||||||
import { openAppSelectorModal } from '../modals'
|
import { openAppSelectorModal } from '../modals'
|
||||||
import { dashboardTab, isNarrow, setCurrentView, setDashboardTab, setSelectedApp } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { dashboardTab, isNarrow } from '../state'
|
||||||
import {
|
import {
|
||||||
AppSelectorChevron,
|
AppSelectorChevron,
|
||||||
DashboardContainer,
|
DashboardContainer,
|
||||||
|
|
@ -25,14 +26,11 @@ export function DashboardLanding({ render }: { render: () => void }) {
|
||||||
const narrow = isNarrow || undefined
|
const narrow = isNarrow || undefined
|
||||||
|
|
||||||
const openSettings = () => {
|
const openSettings = () => {
|
||||||
setSelectedApp(null)
|
navigate('/settings')
|
||||||
setCurrentView('settings')
|
|
||||||
render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const switchTab = (tab: typeof dashboardTab) => {
|
const switchTab = (tab: typeof dashboardTab) => {
|
||||||
setDashboardTab(tab)
|
navigate(tab === 'urls' ? '/' : `/${tab}`)
|
||||||
render()
|
|
||||||
if (tab === 'logs') scrollLogsToBottom()
|
if (tab === 'logs') scrollLogsToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { App } from '../../shared/types'
|
import type { App } from '../../shared/types'
|
||||||
import { apps, getSelectedTab, setSelectedTab } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { apps, getSelectedTab } from '../state'
|
||||||
import { Tab, TabBar } from '../styles'
|
import { Tab, TabBar } from '../styles'
|
||||||
import { resetToolIframe } from '../tool-iframes'
|
import { resetToolIframe } from '../tool-iframes'
|
||||||
|
|
||||||
|
|
@ -12,8 +13,7 @@ export function Nav({ app, render }: { app: App; render: () => void }) {
|
||||||
resetToolIframe(tab, app.name)
|
resetToolIframe(tab, app.name)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
setSelectedTab(app.name, tab)
|
navigate(tab === 'overview' ? `/app/${app.name}` : `/app/${app.name}/${tab}`)
|
||||||
render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all tools
|
// Find all tools
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect, useState } from 'hono/jsx'
|
import { useEffect, useState } from 'hono/jsx'
|
||||||
import { getWifiConfig, saveWifiConfig } from '../api'
|
import { getWifiConfig, saveWifiConfig } from '../api'
|
||||||
import { setCurrentView } from '../state'
|
import { navigate } from '../router'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
DashboardInstallCmd,
|
DashboardInstallCmd,
|
||||||
|
|
@ -31,8 +31,7 @@ export function SettingsPage({ render }: { render: () => void }) {
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const goBack = () => {
|
const goBack = () => {
|
||||||
setCurrentView('dashboard')
|
navigate('/')
|
||||||
render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSave = async (e: Event) => {
|
const handleSave = async (e: Event) => {
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
import { openNewAppModal } from '../modals'
|
import { openNewAppModal } from '../modals'
|
||||||
import {
|
import {
|
||||||
setCurrentView,
|
|
||||||
setSelectedApp,
|
|
||||||
setSidebarCollapsed,
|
setSidebarCollapsed,
|
||||||
sidebarCollapsed,
|
sidebarCollapsed,
|
||||||
} from '../state'
|
} from '../state'
|
||||||
|
|
@ -17,12 +15,6 @@ import {
|
||||||
import { AppSelector } from './AppSelector'
|
import { AppSelector } from './AppSelector'
|
||||||
|
|
||||||
export function Sidebar({ render }: { render: () => void }) {
|
export function Sidebar({ render }: { render: () => void }) {
|
||||||
const goToDashboard = () => {
|
|
||||||
setSelectedApp(null)
|
|
||||||
setCurrentView('dashboard')
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
setSidebarCollapsed(!sidebarCollapsed)
|
setSidebarCollapsed(!sidebarCollapsed)
|
||||||
render()
|
render()
|
||||||
|
|
@ -40,7 +32,7 @@ export function Sidebar({ render }: { render: () => void }) {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Logo>
|
<Logo>
|
||||||
<LogoLink onClick={goToDashboard} title="Go to dashboard">
|
<LogoLink href="/" title="Go to dashboard">
|
||||||
🐾 Toes
|
🐾 Toes
|
||||||
</LogoLink>
|
</LogoLink>
|
||||||
<HamburgerButton onClick={toggleSidebar} title="Hide sidebar">
|
<HamburgerButton onClick={toggleSidebar} title="Hide sidebar">
|
||||||
|
|
@ -50,7 +42,7 @@ export function Sidebar({ render }: { render: () => void }) {
|
||||||
</HamburgerButton>
|
</HamburgerButton>
|
||||||
</Logo>
|
</Logo>
|
||||||
)}
|
)}
|
||||||
<AppSelector render={render} collapsed={sidebarCollapsed} onDashboard={goToDashboard} />
|
<AppSelector render={render} collapsed={sidebarCollapsed} />
|
||||||
{!sidebarCollapsed && (
|
{!sidebarCollapsed && (
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { buildAppUrl } from '../../shared/urls'
|
import { buildAppUrl } from '../../shared/urls'
|
||||||
import { apps, setSelectedApp } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { apps } from '../state'
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
EmptyState,
|
||||||
Tile,
|
Tile,
|
||||||
|
|
@ -22,20 +23,19 @@ export function Urls({ render }: { render: () => void }) {
|
||||||
{nonTools.map(app => {
|
{nonTools.map(app => {
|
||||||
const url = buildAppUrl(app.name, location.origin)
|
const url = buildAppUrl(app.name, location.origin)
|
||||||
const running = app.state === 'running'
|
const running = app.state === 'running'
|
||||||
|
const appPage = `/app/${app.name}`
|
||||||
|
|
||||||
const openAppPage = (e: MouseEvent) => {
|
const openAppPage = (e: MouseEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setSelectedApp(app.name)
|
navigate(appPage)
|
||||||
render()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tile
|
<Tile
|
||||||
key={app.name}
|
key={app.name}
|
||||||
href={running ? url : '#'}
|
href={running ? url : appPage}
|
||||||
target={running ? '_blank' : undefined}
|
target={running ? '_blank' : undefined}
|
||||||
onClick={running ? undefined : openAppPage}
|
|
||||||
>
|
>
|
||||||
<TileStatus state={app.state} onClick={openAppPage} />
|
<TileStatus state={app.state} onClick={openAppPage} />
|
||||||
<TileIcon>{app.icon}</TileIcon>
|
<TileIcon>{app.icon}</TileIcon>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import { render as renderApp } from 'hono/jsx/dom'
|
import { render as renderApp } from 'hono/jsx/dom'
|
||||||
import { Dashboard } from './components'
|
import { Dashboard } from './components'
|
||||||
import { apps, getSelectedTab, selectedApp, setApps, setIsNarrow, setSelectedApp } from './state'
|
|
||||||
import { initModal } from './components/modal'
|
import { initModal } from './components/modal'
|
||||||
|
import { initRouter, navigate } from './router'
|
||||||
|
import { apps, getSelectedTab, selectedApp, setApps, setIsNarrow } from './state'
|
||||||
import { initToolIframes, updateToolIframes } from './tool-iframes'
|
import { initToolIframes, updateToolIframes } from './tool-iframes'
|
||||||
import { initUpdate } from './update'
|
import { initUpdate } from './update'
|
||||||
|
|
||||||
|
|
@ -41,14 +42,16 @@ narrowQuery.addEventListener('change', e => {
|
||||||
render()
|
render()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Initialize router (sets initial state from URL and renders)
|
||||||
|
initRouter(render)
|
||||||
|
|
||||||
// SSE connection
|
// SSE connection
|
||||||
const events = new EventSource('/api/apps/stream')
|
const events = new EventSource('/api/apps/stream')
|
||||||
events.onmessage = e => {
|
events.onmessage = e => {
|
||||||
const prev = apps
|
|
||||||
setApps(JSON.parse(e.data))
|
setApps(JSON.parse(e.data))
|
||||||
|
|
||||||
if (selectedApp && !apps.some(a => a.name === selectedApp)) {
|
if (selectedApp && !apps.some(a => a.name === selectedApp)) {
|
||||||
setSelectedApp(null)
|
navigate('/')
|
||||||
}
|
}
|
||||||
|
|
||||||
render()
|
render()
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { App } from '../../shared/types'
|
import type { App } from '../../shared/types'
|
||||||
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
||||||
import { selectedApp, setSelectedApp } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { selectedApp } from '../state'
|
||||||
import { Button, Form, FormActions, FormError, FormField, FormInput, FormLabel } from '../styles'
|
import { Button, Form, FormActions, FormError, FormField, FormInput, FormLabel } from '../styles'
|
||||||
import { theme } from '../themes'
|
import { theme } from '../themes'
|
||||||
|
|
||||||
|
|
@ -32,11 +33,11 @@ async function deleteApp(input: HTMLInputElement) {
|
||||||
throw new Error(`Failed to delete app: ${res.statusText}`)
|
throw new Error(`Failed to delete app: ${res.statusText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - close modal and clear selection
|
// Success - close modal and navigate to dashboard
|
||||||
if (selectedApp === deleteAppTarget.name) {
|
|
||||||
setSelectedApp(null)
|
|
||||||
}
|
|
||||||
closeModal()
|
closeModal()
|
||||||
|
if (selectedApp === deleteAppTarget.name) {
|
||||||
|
navigate('/')
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
deleteAppError = err instanceof Error ? err.message : 'Failed to delete app'
|
deleteAppError = err instanceof Error ? err.message : 'Failed to delete app'
|
||||||
deleteAppDeleting = false
|
deleteAppDeleting = false
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
||||||
import { apps, setSelectedApp } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { apps } from '../state'
|
||||||
import { Button, Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel, FormError, FormField, FormInput, FormLabel, FormSelect } from '../styles'
|
import { Button, Form, FormActions, FormCheckbox, FormCheckboxField, FormCheckboxLabel, FormError, FormField, FormInput, FormLabel, FormSelect } from '../styles'
|
||||||
|
|
||||||
type TemplateType = 'ssr' | 'spa' | 'bare'
|
type TemplateType = 'ssr' | 'spa' | 'bare'
|
||||||
|
|
@ -48,9 +49,9 @@ async function createNewApp() {
|
||||||
throw new Error(data.error || 'Failed to create app')
|
throw new Error(data.error || 'Failed to create app')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - close modal and select the new app
|
// Success - close modal and navigate to the new app
|
||||||
setSelectedApp(name)
|
|
||||||
closeModal()
|
closeModal()
|
||||||
|
navigate(`/app/${name}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
newAppError = err instanceof Error ? err.message : 'Failed to create app'
|
newAppError = err instanceof Error ? err.message : 'Failed to create app'
|
||||||
newAppCreating = false
|
newAppCreating = false
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import type { App } from '../../shared/types'
|
import type { App } from '../../shared/types'
|
||||||
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
import { closeModal, openModal, rerenderModal } from '../components/modal'
|
||||||
import { apps, setSelectedApp } from '../state'
|
import { navigate } from '../router'
|
||||||
|
import { apps } from '../state'
|
||||||
import { Button, Form, FormActions, FormError, FormField, FormInput, FormLabel } from '../styles'
|
import { Button, Form, FormActions, FormError, FormField, FormInput, FormLabel } from '../styles'
|
||||||
|
|
||||||
let renameAppError = ''
|
let renameAppError = ''
|
||||||
|
|
@ -58,9 +59,9 @@ async function doRenameApp(input: HTMLInputElement) {
|
||||||
throw new Error(data.error || 'Failed to rename app')
|
throw new Error(data.error || 'Failed to rename app')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success - update selection and close modal
|
// Success - close modal and navigate to renamed app
|
||||||
setSelectedApp(data.name || newName)
|
|
||||||
closeModal()
|
closeModal()
|
||||||
|
navigate(`/app/${data.name || newName}`)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
renameAppError = err instanceof Error ? err.message : 'Failed to rename app'
|
renameAppError = err instanceof Error ? err.message : 'Failed to rename app'
|
||||||
renameAppRenaming = false
|
renameAppRenaming = false
|
||||||
|
|
|
||||||
59
src/client/router.ts
Normal file
59
src/client/router.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { setCurrentView, setDashboardTab, setSelectedApp, setSelectedTab } from './state'
|
||||||
|
|
||||||
|
let _render: () => void
|
||||||
|
|
||||||
|
export function navigate(href: string) {
|
||||||
|
history.pushState(null, '', href)
|
||||||
|
route()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initRouter(render: () => void) {
|
||||||
|
_render = render
|
||||||
|
|
||||||
|
// Intercept link clicks
|
||||||
|
document.addEventListener('click', e => {
|
||||||
|
const a = (e.target as Element).closest('a')
|
||||||
|
if (!a || !a.href || a.origin !== location.origin || a.target === '_blank') return
|
||||||
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
||||||
|
e.preventDefault()
|
||||||
|
history.pushState(null, '', a.href)
|
||||||
|
route()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Handle back/forward
|
||||||
|
window.addEventListener('popstate', route)
|
||||||
|
|
||||||
|
// Initial route from URL
|
||||||
|
route()
|
||||||
|
}
|
||||||
|
|
||||||
|
function route() {
|
||||||
|
const path = location.pathname
|
||||||
|
|
||||||
|
if (path.startsWith('/app/')) {
|
||||||
|
const rest = decodeURIComponent(path.slice(5))
|
||||||
|
const slashIdx = rest.indexOf('/')
|
||||||
|
const name = slashIdx === -1 ? rest : rest.slice(0, slashIdx)
|
||||||
|
const tab = slashIdx === -1 ? 'overview' : rest.slice(slashIdx + 1)
|
||||||
|
setSelectedApp(name)
|
||||||
|
setSelectedTab(name, tab)
|
||||||
|
setCurrentView('dashboard')
|
||||||
|
} else if (path === '/settings') {
|
||||||
|
setSelectedApp(null)
|
||||||
|
setCurrentView('settings')
|
||||||
|
} else if (path === '/logs') {
|
||||||
|
setSelectedApp(null)
|
||||||
|
setDashboardTab('logs')
|
||||||
|
setCurrentView('dashboard')
|
||||||
|
} else if (path === '/metrics') {
|
||||||
|
setSelectedApp(null)
|
||||||
|
setDashboardTab('metrics')
|
||||||
|
setCurrentView('dashboard')
|
||||||
|
} else {
|
||||||
|
setSelectedApp(null)
|
||||||
|
setDashboardTab('urls')
|
||||||
|
setCurrentView('dashboard')
|
||||||
|
}
|
||||||
|
|
||||||
|
_render()
|
||||||
|
}
|
||||||
|
|
@ -5,21 +5,20 @@ export type DashboardTab = 'urls' | 'logs' | 'metrics'
|
||||||
// UI state (survives re-renders)
|
// UI state (survives re-renders)
|
||||||
export let currentView: 'dashboard' | 'settings' = 'dashboard'
|
export let currentView: 'dashboard' | 'settings' = 'dashboard'
|
||||||
export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
|
export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
|
||||||
export let selectedApp: string | null = localStorage.getItem('selectedApp')
|
export let selectedApp: string | null = null
|
||||||
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||||
export let dashboardTab: DashboardTab = (localStorage.getItem('dashboardTab') as DashboardTab) || 'urls'
|
export let dashboardTab: DashboardTab = 'urls'
|
||||||
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
|
export let sidebarSection: 'apps' | 'tools' = (localStorage.getItem('sidebarSection') as 'apps' | 'tools') || 'apps'
|
||||||
|
|
||||||
// Server state (from SSE)
|
// Server state (from SSE)
|
||||||
export let apps: App[] = []
|
export let apps: App[] = []
|
||||||
|
|
||||||
// Tab state
|
// Tab state
|
||||||
export let appTabs: Record<string, string> = JSON.parse(localStorage.getItem('appTabs') || '{}')
|
export let appTabs: Record<string, string> = {}
|
||||||
|
|
||||||
// State setters
|
// State setters
|
||||||
export function setDashboardTab(tab: DashboardTab) {
|
export function setDashboardTab(tab: DashboardTab) {
|
||||||
dashboardTab = tab
|
dashboardTab = tab
|
||||||
localStorage.setItem('dashboardTab', tab)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setCurrentView(view: 'dashboard' | 'settings') {
|
export function setCurrentView(view: 'dashboard' | 'settings') {
|
||||||
|
|
@ -28,11 +27,6 @@ export function setCurrentView(view: 'dashboard' | 'settings') {
|
||||||
|
|
||||||
export function setSelectedApp(name: string | null) {
|
export function setSelectedApp(name: string | null) {
|
||||||
selectedApp = name
|
selectedApp = name
|
||||||
if (name) {
|
|
||||||
localStorage.setItem('selectedApp', name)
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('selectedApp')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function setIsNarrow(narrow: boolean) {
|
export function setIsNarrow(narrow: boolean) {
|
||||||
|
|
@ -59,5 +53,4 @@ export const getSelectedTab = (appName: string | null) =>
|
||||||
export function setSelectedTab(appName: string | null, tab: string) {
|
export function setSelectedTab(appName: string | null, tab: string) {
|
||||||
if (!appName) return
|
if (!appName) return
|
||||||
appTabs[appName] = tab
|
appTabs[appName] = tab
|
||||||
localStorage.setItem('appTabs', JSON.stringify(appTabs))
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,10 @@ export const Logo = define('Logo', {
|
||||||
})
|
})
|
||||||
|
|
||||||
export const LogoLink = define('LogoLink', {
|
export const LogoLink = define('LogoLink', {
|
||||||
|
base: 'a',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
borderRadius: theme('radius-md'),
|
borderRadius: theme('radius-md'),
|
||||||
padding: '4px 8px',
|
padding: '4px 8px',
|
||||||
margin: '-4px -8px',
|
margin: '-4px -8px',
|
||||||
|
|
@ -112,6 +115,7 @@ export const AppList = define('AppList', {
|
||||||
})
|
})
|
||||||
|
|
||||||
export const AppItem = define('AppItem', {
|
export const AppItem = define('AppItem', {
|
||||||
|
base: 'a',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import systemRouter from './api/system'
|
||||||
import { Hype } from '@because/hype'
|
import { Hype } from '@because/hype'
|
||||||
import { cleanupStalePublishers } from './mdns'
|
import { cleanupStalePublishers } from './mdns'
|
||||||
import { extractSubdomain, proxySubdomain, proxyWebSocket, websocket } from './proxy'
|
import { extractSubdomain, proxySubdomain, proxyWebSocket, websocket } from './proxy'
|
||||||
|
import { Shell } from './shell'
|
||||||
import type { Server } from 'bun'
|
import type { Server } from 'bun'
|
||||||
import type { WsData } from './proxy'
|
import type { WsData } from './proxy'
|
||||||
|
|
||||||
|
|
@ -113,6 +114,13 @@ app.get('/dist/:file', async c => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// SPA routes — serve the shell for all client-side paths
|
||||||
|
app.get('/app/:name/:tab', c => c.html(<Shell />))
|
||||||
|
app.get('/app/:name', c => c.html(<Shell />))
|
||||||
|
app.get('/logs', c => c.html(<Shell />))
|
||||||
|
app.get('/metrics', c => c.html(<Shell />))
|
||||||
|
app.get('/settings', c => c.html(<Shell />))
|
||||||
|
|
||||||
cleanupStalePublishers()
|
cleanupStalePublishers()
|
||||||
await initApps()
|
await initApps()
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user