dashboard
This commit is contained in:
parent
071f1a02b5
commit
971ebef21c
|
|
@ -4,6 +4,16 @@ export const getLogDates = (name: string): Promise<string[]> =>
|
|||
export const getLogsForDate = (name: string, date: string): Promise<string[]> =>
|
||||
fetch(`/api/apps/${name}/logs?date=${date}`).then(r => r.json())
|
||||
|
||||
export const getWifiConfig = (): Promise<{ network: string, password: string }> =>
|
||||
fetch('/api/system/wifi').then(r => r.json())
|
||||
|
||||
export const saveWifiConfig = (config: { network: string, password: string }) =>
|
||||
fetch('/api/system/wifi', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(config),
|
||||
}).then(r => r.json())
|
||||
|
||||
export const shareApp = (name: string) =>
|
||||
fetch(`/api/apps/${name}/tunnel`, { method: 'POST' })
|
||||
|
||||
|
|
|
|||
|
|
@ -1,23 +1,25 @@
|
|||
import { Styles } from '@because/forge'
|
||||
import { apps, isNarrow, selectedApp } from '../state'
|
||||
import { apps, currentView, isNarrow, selectedApp } from '../state'
|
||||
import { Layout } from '../styles'
|
||||
import { AppDetail } from './AppDetail'
|
||||
import { DashboardLanding } from './DashboardLanding'
|
||||
import { Modal } from './modal'
|
||||
import { SettingsPage } from './SettingsPage'
|
||||
import { Sidebar } from './Sidebar'
|
||||
|
||||
export function Dashboard({ render }: { render: () => void }) {
|
||||
function MainContent({ render }: { render: () => void }) {
|
||||
const selected = apps.find(a => a.name === selectedApp)
|
||||
if (selected) return <AppDetail app={selected} render={render} />
|
||||
if (currentView === 'settings') return <SettingsPage render={render} />
|
||||
return <DashboardLanding render={render} />
|
||||
}
|
||||
|
||||
export function Dashboard({ render }: { render: () => void }) {
|
||||
return (
|
||||
<Layout>
|
||||
<Styles />
|
||||
{!isNarrow && <Sidebar render={render} />}
|
||||
{selected ? (
|
||||
<AppDetail app={selected} render={render} />
|
||||
) : (
|
||||
<DashboardLanding render={render} />
|
||||
)}
|
||||
<MainContent render={render} />
|
||||
<Modal />
|
||||
</Layout>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { useEffect } from 'hono/jsx'
|
||||
import { openAppSelectorModal } from '../modals'
|
||||
import { apps, isNarrow, setSelectedApp } from '../state'
|
||||
import { apps, isNarrow, setCurrentView, setSelectedApp } from '../state'
|
||||
import {
|
||||
AppSelectorChevron,
|
||||
DashboardContainer,
|
||||
DashboardHeader,
|
||||
DashboardInstallCmd,
|
||||
DashboardTitle,
|
||||
SettingsGear,
|
||||
StatusDot,
|
||||
StatusDotLink,
|
||||
StatusDotsRow,
|
||||
|
|
@ -25,8 +26,21 @@ export function DashboardLanding({ render }: { render: () => void }) {
|
|||
|
||||
const narrow = isNarrow || undefined
|
||||
|
||||
const openSettings = () => {
|
||||
setSelectedApp(null)
|
||||
setCurrentView('settings')
|
||||
render()
|
||||
}
|
||||
|
||||
return (
|
||||
<DashboardContainer narrow={narrow}>
|
||||
<DashboardContainer narrow={narrow} relative>
|
||||
<SettingsGear
|
||||
onClick={openSettings}
|
||||
title="Settings"
|
||||
style={{ position: 'absolute', top: 16, right: 16 }}
|
||||
>
|
||||
⚙️
|
||||
</SettingsGear>
|
||||
<DashboardHeader>
|
||||
<DashboardTitle narrow={narrow}>
|
||||
🐾 Toes
|
||||
|
|
|
|||
87
src/client/components/SettingsPage.tsx
Normal file
87
src/client/components/SettingsPage.tsx
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
import { useEffect, useState } from 'hono/jsx'
|
||||
import { getWifiConfig, saveWifiConfig } from '../api'
|
||||
import { setCurrentView } from '../state'
|
||||
import {
|
||||
Button,
|
||||
FormActions,
|
||||
FormField,
|
||||
FormInput,
|
||||
FormLabel,
|
||||
HeaderActions,
|
||||
Main,
|
||||
MainContent,
|
||||
MainHeader,
|
||||
MainTitle,
|
||||
Section,
|
||||
SectionTitle,
|
||||
} from '../styles'
|
||||
|
||||
export function SettingsPage({ render }: { render: () => void }) {
|
||||
const [network, setNetwork] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [saved, setSaved] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getWifiConfig().then(config => {
|
||||
setNetwork(config.network)
|
||||
setPassword(config.password)
|
||||
})
|
||||
}, [])
|
||||
|
||||
const goBack = () => {
|
||||
setCurrentView('dashboard')
|
||||
render()
|
||||
}
|
||||
|
||||
const handleSave = async (e: Event) => {
|
||||
e.preventDefault()
|
||||
setSaving(true)
|
||||
setSaved(false)
|
||||
await saveWifiConfig({ network, password })
|
||||
setSaving(false)
|
||||
setSaved(true)
|
||||
}
|
||||
|
||||
return (
|
||||
<Main>
|
||||
<MainHeader>
|
||||
<MainTitle>Settings</MainTitle>
|
||||
<HeaderActions>
|
||||
<Button onClick={goBack}>Back</Button>
|
||||
</HeaderActions>
|
||||
</MainHeader>
|
||||
<MainContent>
|
||||
<Section>
|
||||
<SectionTitle>WiFi</SectionTitle>
|
||||
<form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 400 }}>
|
||||
<FormField>
|
||||
<FormLabel>Network</FormLabel>
|
||||
<FormInput
|
||||
type="text"
|
||||
value={network}
|
||||
onInput={(e: Event) => setNetwork((e.target as HTMLInputElement).value)}
|
||||
placeholder="SSID"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormInput
|
||||
type="password"
|
||||
value={password}
|
||||
onInput={(e: Event) => setPassword((e.target as HTMLInputElement).value)}
|
||||
placeholder="Password"
|
||||
/>
|
||||
</FormField>
|
||||
<FormActions>
|
||||
{saved && <span style={{ fontSize: 13, color: '#888', alignSelf: 'center' }}>Saved</span>}
|
||||
<Button variant="primary" type="submit" disabled={saving}>
|
||||
{saving ? 'Saving...' : 'Save'}
|
||||
</Button>
|
||||
</FormActions>
|
||||
</form>
|
||||
</Section>
|
||||
</MainContent>
|
||||
</Main>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { openNewAppModal } from '../modals'
|
||||
import {
|
||||
setCurrentView,
|
||||
setSelectedApp,
|
||||
setSidebarCollapsed,
|
||||
sidebarCollapsed,
|
||||
|
|
@ -18,6 +19,7 @@ import { AppSelector } from './AppSelector'
|
|||
export function Sidebar({ render }: { render: () => void }) {
|
||||
const goToDashboard = () => {
|
||||
setSelectedApp(null)
|
||||
setCurrentView('dashboard')
|
||||
render()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import type { App } from '../shared/types'
|
||||
|
||||
// UI state (survives re-renders)
|
||||
export let currentView: 'dashboard' | 'settings' = 'dashboard'
|
||||
export let isNarrow: boolean = window.matchMedia('(max-width: 768px)').matches
|
||||
export let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||
export let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
||||
|
|
@ -13,6 +14,10 @@ export let apps: App[] = []
|
|||
export let appTabs: Record<string, string> = JSON.parse(localStorage.getItem('appTabs') || '{}')
|
||||
|
||||
// State setters
|
||||
export function setCurrentView(view: 'dashboard' | 'settings') {
|
||||
currentView = view
|
||||
}
|
||||
|
||||
export function setSelectedApp(name: string | null) {
|
||||
selectedApp = name
|
||||
if (name) {
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export {
|
|||
SectionLabel,
|
||||
SectionSwitcher,
|
||||
SectionTab,
|
||||
SettingsGear,
|
||||
Sidebar,
|
||||
SidebarFooter,
|
||||
StatCard,
|
||||
|
|
|
|||
|
|
@ -211,6 +211,9 @@ export const DashboardContainer = define('DashboardContainer', {
|
|||
padding: 20,
|
||||
gap: 24,
|
||||
},
|
||||
relative: {
|
||||
position: 'relative',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
|
|
@ -264,6 +267,23 @@ export const StatValue = define('StatValue', {
|
|||
marginBottom: 4,
|
||||
})
|
||||
|
||||
export const SettingsGear = define('SettingsGear', {
|
||||
base: 'button',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
fontSize: 18,
|
||||
color: theme('colors-textMuted'),
|
||||
padding: '4px 8px',
|
||||
borderRadius: theme('radius-md'),
|
||||
selectors: {
|
||||
'&:hover': {
|
||||
color: theme('colors-text'),
|
||||
background: theme('colors-bgHover'),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export const StatLabel = define('StatLabel', {
|
||||
fontSize: 14,
|
||||
color: theme('colors-textMuted'),
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { allApps, APPS_DIR, onChange } from '$apps'
|
||||
import { allApps, APPS_DIR, onChange, TOES_DIR } from '$apps'
|
||||
import { onHostLog } from '../tui'
|
||||
import { Hype } from '@because/hype'
|
||||
import { cpus, platform, totalmem } from 'os'
|
||||
import { join } from 'path'
|
||||
import { readFileSync, statfsSync } from 'fs'
|
||||
import { existsSync, mkdirSync, readFileSync, statfsSync, writeFileSync } from 'fs'
|
||||
|
||||
export interface AppMetrics {
|
||||
cpu: number
|
||||
|
|
@ -18,6 +18,11 @@ export interface SystemMetrics {
|
|||
apps: Record<string, AppMetrics>
|
||||
}
|
||||
|
||||
export interface WifiConfig {
|
||||
network: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface UnifiedLogLine {
|
||||
time: number
|
||||
app: string
|
||||
|
|
@ -199,6 +204,38 @@ router.sse('/metrics/stream', (send) => {
|
|||
return () => clearInterval(interval)
|
||||
})
|
||||
|
||||
// WiFi config
|
||||
const CONFIG_DIR = join(TOES_DIR, 'config')
|
||||
const WIFI_PATH = join(CONFIG_DIR, 'wifi.json')
|
||||
|
||||
function readWifiConfig(): WifiConfig {
|
||||
try {
|
||||
if (existsSync(WIFI_PATH)) {
|
||||
return JSON.parse(readFileSync(WIFI_PATH, 'utf-8'))
|
||||
}
|
||||
} catch {}
|
||||
return { network: '', password: '' }
|
||||
}
|
||||
|
||||
function writeWifiConfig(config: WifiConfig) {
|
||||
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true })
|
||||
writeFileSync(WIFI_PATH, JSON.stringify(config, null, 2))
|
||||
}
|
||||
|
||||
router.get('/wifi', c => {
|
||||
return c.json(readWifiConfig())
|
||||
})
|
||||
|
||||
router.put('/wifi', async c => {
|
||||
const body = await c.req.json<WifiConfig>()
|
||||
const config: WifiConfig = {
|
||||
network: String(body.network ?? ''),
|
||||
password: String(body.password ?? ''),
|
||||
}
|
||||
writeWifiConfig(config)
|
||||
return c.json(config)
|
||||
})
|
||||
|
||||
// Get recent unified logs
|
||||
router.get('/logs', c => {
|
||||
const tail = c.req.query('tail')
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user