From 971ebef21c0449d5d1805894074ec22834457e75 Mon Sep 17 00:00:00 2001 From: Chris Wanstrath Date: Thu, 19 Feb 2026 09:28:15 -0800 Subject: [PATCH] dashboard --- src/client/api.ts | 10 +++ src/client/components/Dashboard.tsx | 16 ++-- src/client/components/DashboardLanding.tsx | 18 ++++- src/client/components/SettingsPage.tsx | 87 ++++++++++++++++++++++ src/client/components/Sidebar.tsx | 2 + src/client/state.ts | 5 ++ src/client/styles/index.ts | 1 + src/client/styles/layout.ts | 20 +++++ src/server/api/system.ts | 41 +++++++++- 9 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 src/client/components/SettingsPage.tsx diff --git a/src/client/api.ts b/src/client/api.ts index 1671b3f..6b7143f 100644 --- a/src/client/api.ts +++ b/src/client/api.ts @@ -4,6 +4,16 @@ export const getLogDates = (name: string): Promise => export const getLogsForDate = (name: string, date: string): Promise => 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' }) diff --git a/src/client/components/Dashboard.tsx b/src/client/components/Dashboard.tsx index aeb0996..c88e014 100644 --- a/src/client/components/Dashboard.tsx +++ b/src/client/components/Dashboard.tsx @@ -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 + if (currentView === 'settings') return + return +} +export function Dashboard({ render }: { render: () => void }) { return ( {!isNarrow && } - {selected ? ( - - ) : ( - - )} + ) diff --git a/src/client/components/DashboardLanding.tsx b/src/client/components/DashboardLanding.tsx index 6899adc..6800749 100644 --- a/src/client/components/DashboardLanding.tsx +++ b/src/client/components/DashboardLanding.tsx @@ -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 ( - + + + ⚙️ + 🐾 Toes diff --git a/src/client/components/SettingsPage.tsx b/src/client/components/SettingsPage.tsx new file mode 100644 index 0000000..bebaf1f --- /dev/null +++ b/src/client/components/SettingsPage.tsx @@ -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 ( +
+ + Settings + + + + + +
+ WiFi +
+ + Network + setNetwork((e.target as HTMLInputElement).value)} + placeholder="SSID" + /> + + + Password + setPassword((e.target as HTMLInputElement).value)} + placeholder="Password" + /> + + + {saved && Saved} + + +
+
+
+
+ ) +} diff --git a/src/client/components/Sidebar.tsx b/src/client/components/Sidebar.tsx index ba586cc..141bcca 100644 --- a/src/client/components/Sidebar.tsx +++ b/src/client/components/Sidebar.tsx @@ -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() } diff --git a/src/client/state.ts b/src/client/state.ts index bc59146..a01c574 100644 --- a/src/client/state.ts +++ b/src/client/state.ts @@ -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 = 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) { diff --git a/src/client/styles/index.ts b/src/client/styles/index.ts index 3275071..402c6b7 100644 --- a/src/client/styles/index.ts +++ b/src/client/styles/index.ts @@ -43,6 +43,7 @@ export { SectionLabel, SectionSwitcher, SectionTab, + SettingsGear, Sidebar, SidebarFooter, StatCard, diff --git a/src/client/styles/layout.ts b/src/client/styles/layout.ts index a223607..4148285 100644 --- a/src/client/styles/layout.ts +++ b/src/client/styles/layout.ts @@ -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'), diff --git a/src/server/api/system.ts b/src/server/api/system.ts index f6fe719..87f0c5e 100644 --- a/src/server/api/system.ts +++ b/src/server/api/system.ts @@ -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 } +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() + 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')