partial updating

This commit is contained in:
Chris Wanstrath 2026-01-28 21:33:49 -08:00
parent 5d898ac485
commit 2c8fff85f4
6 changed files with 1185 additions and 40 deletions

View File

@ -3,14 +3,13 @@ import { define, Styles } from 'forge'
import type { App, AppState } from '../shared/types'
import { theme } from './themes'
import { Modal, initModal } from './tags/modal'
import { initUpdate } from './update'
import { openEmojiPicker } from './tags/emoji-picker'
// UI state (survives re-renders)
let selectedApp: string | null = localStorage.getItem('selectedApp')
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
const DEFAULT_EMOJI = '🖥️'
// Server state (from SSE)
let apps: App[] = []
@ -382,7 +381,7 @@ const AppDetail = ({ app }: { app: App }) => (
<>
<MainHeader>
<MainTitle>
<OpenEmojiPicker app={app}>{app.icon ?? DEFAULT_EMOJI}</OpenEmojiPicker>
<OpenEmojiPicker app={app}>{app.icon}</OpenEmojiPicker>
&nbsp;
{app.name}
</MainTitle>
@ -510,10 +509,10 @@ const Dashboard = () => {
title={sidebarCollapsed ? app.name : undefined}
>
{sidebarCollapsed ? (
<span style={{ fontSize: 18 }}>{app.icon ?? DEFAULT_EMOJI}</span>
<span style={{ fontSize: 18 }}>{app.icon}</span>
) : (
<>
<span style={{ fontSize: 14 }}>{app.icon ?? DEFAULT_EMOJI}</span>
<span style={{ fontSize: 14 }}>{app.icon}</span>
{app.name}
<StatusDot state={app.state} style={{ marginLeft: 'auto' }} />
</>
@ -543,8 +542,9 @@ const render = () => {
renderApp(<Dashboard />, document.getElementById('app')!)
}
// Initialize modal with render function
// Initialize render functions
initModal(render)
initUpdate(render)
// Set theme based on system preference
const setTheme = () => {

File diff suppressed because it is too large Load Diff

25
src/client/update.tsx Normal file
View File

@ -0,0 +1,25 @@
import { render } from 'hono/jsx/dom'
import type { Child } from 'hono/jsx'
let globalRenderFn: (() => void) | null = null
export const initUpdate = (renderFn: () => void) => {
globalRenderFn = renderFn
}
/**
* Update the UI from state.
*
* update() - redraw everything
* update('#emoji-results', <Results />) - target specific element
*/
export function update(): void
export function update(selector: string, component: Child): void
export function update(selector?: string, component?: Child) {
if (selector && component !== undefined) {
const el = document.querySelector(selector) as HTMLElement | null
if (el) render(component, el)
} else {
globalRenderFn?.()
}
}

View File

@ -8,6 +8,7 @@ import type { App as SharedApp, AppState, LogLine } from '../shared/types'
export type { AppState } from '../shared/types'
const DEFAULT_EMOJI = '🖥️'
const APPS_DIR = join(process.env.DATA_DIR ?? '.', 'apps')
const MAX_LOGS = 100
@ -42,29 +43,20 @@ const allAppDirs = () => {
.sort()
}
/** Returns names of valid apps (those with scripts.toes in package.json) */
export const appNames = () => allAppDirs().filter(isApp)
let NEXT_PORT = 3001
const getPort = () => NEXT_PORT++
/** Discover all apps and set initial states */
const discoverApps = () => {
for (const dir of allAppDirs()) {
const { pkg, error } = loadApp(dir)
const state: AppState = error ? 'invalid' : 'stopped'
const icon = pkg.toes?.icon
const icon = pkg.toes?.icon ?? DEFAULT_EMOJI
_apps.set(dir, { name: dir, state, icon, error })
}
}
/** Start all valid apps */
export const runApps = () => {
for (const dir of appNames()) {
const port = getPort()
runApp(dir, port)
}
}
export const runApps = () =>
allAppDirs().filter(isApp).forEach(startApp)
type LoadResult = { pkg: any; error?: string }
@ -197,6 +189,7 @@ export const getApp = (dir: string): App | undefined => _apps.get(dir)
export const startApp = (dir: string) => {
const app = _apps.get(dir)
if (!app || app.state !== 'stopped') return
if (!isApp(dir)) return
runApp(dir, getPort())
}

View File

@ -50,6 +50,16 @@ app.get('/api/apps/stream', c => {
})
})
app.get('/api/apps', c => {
const apps = allApps().map(app => {
const clone = { ...app }
delete clone.proc
delete clone.logs
return clone
})
return c.json(apps)
})
app.post('/api/apps/:app/start', c => {
const appName = c.req.param('app')
if (!appName) return c.json({ error: 'App not found' }, 404)

View File

@ -8,7 +8,7 @@ export type LogLine = {
export type App = {
name: string
state: AppState
icon?: string
icon: string
error?: string
port?: number
started?: number