Compare commits
No commits in common. "e2ff53e6d8800c31708c50fe3c08018140c23cf9" and "06bcfc5f35ea4804bb4c53b64a4edf5009362bfe" have entirely different histories.
e2ff53e6d8
...
06bcfc5f35
2
bun.lock
2
bun.lock
|
|
@ -27,7 +27,7 @@
|
||||||
|
|
||||||
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
"hono": ["hono@4.11.7", "", {}, "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw=="],
|
||||||
|
|
||||||
"hype": ["hype@git+https://git.nose.space/defunkt/hype#1a8d4228347a47b696b4c6918179cda94fc1b0cd", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "1a8d4228347a47b696b4c6918179cda94fc1b0cd"],
|
"hype": ["hype@git+https://git.nose.space/defunkt/hype#a8d3a8203e145df7a222ea409588c2ea3a1ee4e6", { "dependencies": { "hono": "^4.10.4", "kleur": "^4.1.5" }, "peerDependencies": { "typescript": "^5" } }, "a8d3a8203e145df7a222ea409588c2ea3a1ee4e6"],
|
||||||
|
|
||||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { theme } from './themes'
|
||||||
|
|
||||||
// UI state (survives re-renders)
|
// UI state (survives re-renders)
|
||||||
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
let selectedApp: string | null = localStorage.getItem('selectedApp')
|
||||||
let sidebarCollapsed: boolean = localStorage.getItem('sidebarCollapsed') === 'true'
|
|
||||||
|
|
||||||
// Server state (from SSE)
|
// Server state (from SSE)
|
||||||
let apps: App[] = []
|
let apps: App[] = []
|
||||||
|
|
@ -31,36 +30,12 @@ const Logo = define('Logo', {
|
||||||
height: 64,
|
height: 64,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'space-between',
|
|
||||||
padding: '0 16px',
|
padding: '0 16px',
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
borderBottom: `1px solid ${theme('colors-border')}`,
|
borderBottom: `1px solid ${theme('colors-border')}`,
|
||||||
})
|
})
|
||||||
|
|
||||||
const HamburgerButton = define('HamburgerButton', {
|
|
||||||
base: 'button',
|
|
||||||
background: 'none',
|
|
||||||
border: 'none',
|
|
||||||
cursor: 'pointer',
|
|
||||||
padding: 4,
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
justifyContent: 'center',
|
|
||||||
gap: 4,
|
|
||||||
selectors: {
|
|
||||||
'&:hover span': { background: theme('colors-text') },
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const HamburgerLine = define('HamburgerLine', {
|
|
||||||
width: 18,
|
|
||||||
height: 2,
|
|
||||||
background: theme('colors-textMuted'),
|
|
||||||
borderRadius: 1,
|
|
||||||
transition: 'background 0.15s',
|
|
||||||
})
|
|
||||||
|
|
||||||
const SectionLabel = define('SectionLabel', {
|
const SectionLabel = define('SectionLabel', {
|
||||||
padding: '16px 16px 8px',
|
padding: '16px 16px 8px',
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
|
|
@ -166,7 +141,7 @@ const HeaderActions = define('HeaderActions', {
|
||||||
|
|
||||||
const MainContent = define('MainContent', {
|
const MainContent = define('MainContent', {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
padding: '10px 24px',
|
padding: 24,
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -273,43 +248,8 @@ const LogLine = define('LogLine', {
|
||||||
const LogTime = define('LogTime', {
|
const LogTime = define('LogTime', {
|
||||||
color: theme('colors-textFaintest'),
|
color: theme('colors-textFaintest'),
|
||||||
marginRight: 12,
|
marginRight: 12,
|
||||||
display: 'inline',
|
|
||||||
})
|
})
|
||||||
|
|
||||||
let selectedTab: 'overview' | 'todo' = 'overview'
|
|
||||||
|
|
||||||
const TabContent = define('TabContent', {
|
|
||||||
display: 'none',
|
|
||||||
|
|
||||||
variants: {
|
|
||||||
active: {
|
|
||||||
display: 'block'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const NavButton = define('NavButton', {
|
|
||||||
render({ props }) {
|
|
||||||
return (
|
|
||||||
<Button onClick={props.onClick} style={{ marginRight: 10, marginBottom: 20 }}>
|
|
||||||
{props.children}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function setSelectedTab(tab: 'overview' | 'todo') {
|
|
||||||
selectedTab = tab
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const Nav = () => {
|
|
||||||
return <>
|
|
||||||
<NavButton onClick={() => setSelectedTab('overview')}>Overview</NavButton>
|
|
||||||
<NavButton onClick={() => setSelectedTab('todo')}>TODO</NavButton>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
|
|
||||||
const stateLabels: Record<AppState, string> = {
|
const stateLabels: Record<AppState, string> = {
|
||||||
invalid: 'Invalid',
|
invalid: 'Invalid',
|
||||||
stopped: 'Stopped',
|
stopped: 'Stopped',
|
||||||
|
|
@ -329,17 +269,13 @@ const selectApp = (name: string) => {
|
||||||
render()
|
render()
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
|
||||||
sidebarCollapsed = !sidebarCollapsed
|
|
||||||
localStorage.setItem('sidebarCollapsed', String(sidebarCollapsed))
|
|
||||||
render()
|
|
||||||
}
|
|
||||||
|
|
||||||
const AppDetail = ({ app }: { app: App }) => (
|
const AppDetail = ({ app }: { app: App }) => (
|
||||||
<>
|
<>
|
||||||
<MainHeader>
|
<MainHeader>
|
||||||
<MainTitle>
|
<MainTitle>
|
||||||
{app.icon ?? <StatusDot state={app.state} />}
|
{app.state === 'running' && app.icon ? <>{app.icon}</> : (
|
||||||
|
<StatusDot state={app.state} />
|
||||||
|
)}
|
||||||
|
|
||||||
{app.name}
|
{app.name}
|
||||||
</MainTitle>
|
</MainTitle>
|
||||||
|
|
@ -349,87 +285,79 @@ const AppDetail = ({ app }: { app: App }) => (
|
||||||
</HeaderActions>
|
</HeaderActions>
|
||||||
</MainHeader>
|
</MainHeader>
|
||||||
<MainContent>
|
<MainContent>
|
||||||
<Nav />
|
<Section>
|
||||||
|
<SectionTitle>Status</SectionTitle>
|
||||||
<TabContent active={selectedTab === 'overview'}>
|
<InfoRow>
|
||||||
<Section>
|
<InfoLabel>State</InfoLabel>
|
||||||
<SectionTitle>Status</SectionTitle>
|
<InfoValue>
|
||||||
|
<StatusDot state={app.state} />
|
||||||
|
{stateLabels[app.state]}
|
||||||
|
{app.port ? ` on :${app.port}` : ''}
|
||||||
|
</InfoValue>
|
||||||
|
</InfoRow>
|
||||||
|
{app.state === 'running' && app.port && (
|
||||||
<InfoRow>
|
<InfoRow>
|
||||||
<InfoLabel>State</InfoLabel>
|
<InfoLabel>URL</InfoLabel>
|
||||||
<InfoValue>
|
<InfoValue>
|
||||||
<StatusDot state={app.state} />
|
<Link href={`http://localhost:${app.port}`} target="_blank">
|
||||||
{stateLabels[app.state]}
|
http://localhost:{app.port}
|
||||||
{app.port ? ` on :${app.port}` : ''}
|
</Link>
|
||||||
</InfoValue>
|
</InfoValue>
|
||||||
</InfoRow>
|
</InfoRow>
|
||||||
{app.state === 'running' && app.port && (
|
)}
|
||||||
<InfoRow>
|
{app.started && (
|
||||||
<InfoLabel>URL</InfoLabel>
|
<InfoRow>
|
||||||
<InfoValue>
|
<InfoLabel>Started</InfoLabel>
|
||||||
<Link href={`http://localhost:${app.port}`} target="_blank">
|
<InfoValue>{new Date(app.started).toLocaleString()}</InfoValue>
|
||||||
http://localhost:{app.port}
|
</InfoRow>
|
||||||
</Link>
|
)}
|
||||||
</InfoValue>
|
{app.error && (
|
||||||
</InfoRow>
|
<InfoRow>
|
||||||
)}
|
<InfoLabel>Error</InfoLabel>
|
||||||
{app.started && (
|
<InfoValue style={{ color: theme('colors-error') }}>
|
||||||
<InfoRow>
|
{app.error}
|
||||||
<InfoLabel>Started</InfoLabel>
|
</InfoValue>
|
||||||
<InfoValue>{new Date(app.started).toLocaleString()}</InfoValue>
|
</InfoRow>
|
||||||
</InfoRow>
|
)}
|
||||||
)}
|
</Section>
|
||||||
{app.error && (
|
|
||||||
<InfoRow>
|
|
||||||
<InfoLabel>Error</InfoLabel>
|
|
||||||
<InfoValue style={{ color: theme('colors-error') }}>
|
|
||||||
{app.error}
|
|
||||||
</InfoValue>
|
|
||||||
</InfoRow>
|
|
||||||
)}
|
|
||||||
</Section>
|
|
||||||
|
|
||||||
<Section>
|
<Section>
|
||||||
<SectionTitle>Logs</SectionTitle>
|
<SectionTitle>Logs</SectionTitle>
|
||||||
<LogsContainer>
|
<LogsContainer>
|
||||||
{app.logs?.length ? (
|
{app.logs?.length ? (
|
||||||
app.logs.map((line, i) => (
|
app.logs.map((line, i) => (
|
||||||
<LogLine key={i}>
|
<LogLine key={i}>
|
||||||
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
<LogTime>{new Date(line.time).toLocaleTimeString()}</LogTime>
|
||||||
<span>{line.text}</span>
|
<span>{line.text}</span>
|
||||||
</LogLine>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<LogLine>
|
|
||||||
<LogTime>--:--:--</LogTime>
|
|
||||||
<span style={{ color: theme('colors-textFaint') }}>No logs yet</span>
|
|
||||||
</LogLine>
|
</LogLine>
|
||||||
)}
|
))
|
||||||
</LogsContainer>
|
) : (
|
||||||
</Section>
|
<LogLine>
|
||||||
|
<LogTime>--:--:--</LogTime>
|
||||||
|
<span style={{ color: theme('colors-textFaint') }}>No logs yet</span>
|
||||||
|
</LogLine>
|
||||||
|
)}
|
||||||
|
</LogsContainer>
|
||||||
|
</Section>
|
||||||
|
|
||||||
<ActionBar>
|
<ActionBar>
|
||||||
{app.state === 'stopped' && (
|
{app.state === 'stopped' && (
|
||||||
<Button variant="primary" onClick={() => startApp(app.name)}>
|
<Button variant="primary" onClick={() => startApp(app.name)}>
|
||||||
Start
|
Start
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{app.state === 'running' && (
|
||||||
|
<>
|
||||||
|
<Button onClick={() => restartApp(app.name)}>Restart</Button>
|
||||||
|
<Button variant="danger" onClick={() => stopApp(app.name)}>
|
||||||
|
Stop
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</>
|
||||||
{app.state === 'running' && (
|
)}
|
||||||
<>
|
{(app.state === 'starting' || app.state === 'stopping') && (
|
||||||
<Button onClick={() => restartApp(app.name)}>Restart</Button>
|
<Button disabled>{stateLabels[app.state]}...</Button>
|
||||||
<Button variant="danger" onClick={() => stopApp(app.name)}>
|
)}
|
||||||
Stop
|
</ActionBar>
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(app.state === 'starting' || app.state === 'stopping') && (
|
|
||||||
<Button disabled>{stateLabels[app.state]}...</Button>
|
|
||||||
)}
|
|
||||||
</ActionBar>
|
|
||||||
</TabContent>
|
|
||||||
|
|
||||||
<TabContent active={selectedTab === 'todo'}>
|
|
||||||
<h1>hardy har har</h1>
|
|
||||||
</TabContent>
|
|
||||||
</MainContent>
|
</MainContent>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
@ -440,39 +368,28 @@ const Dashboard = () => {
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Styles />
|
<Styles />
|
||||||
<Sidebar style={sidebarCollapsed ? { width: 'auto' } : undefined}>
|
<Sidebar>
|
||||||
<Logo>
|
<Logo>🐾 Toes</Logo>
|
||||||
{!sidebarCollapsed && <span>🐾 Toes</span>}
|
<SectionLabel>Apps</SectionLabel>
|
||||||
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}>
|
|
||||||
<HamburgerLine />
|
|
||||||
<HamburgerLine />
|
|
||||||
<HamburgerLine />
|
|
||||||
</HamburgerButton>
|
|
||||||
</Logo>
|
|
||||||
{!sidebarCollapsed && <SectionLabel>Apps</SectionLabel>}
|
|
||||||
<AppList>
|
<AppList>
|
||||||
{apps.map(app => (
|
{apps.map(app => (
|
||||||
<AppItem
|
<AppItem
|
||||||
key={app.name}
|
key={app.name}
|
||||||
onClick={() => selectApp(app.name)}
|
onClick={() => selectApp(app.name)}
|
||||||
selected={app.name === selectedApp ? true : undefined}
|
selected={app.name === selectedApp ? true : undefined}
|
||||||
style={sidebarCollapsed ? { justifyContent: 'center', padding: '10px 12px' } : undefined}
|
|
||||||
title={sidebarCollapsed ? app.name : undefined}
|
|
||||||
>
|
>
|
||||||
{app.state === 'running' && app.icon ? (
|
{app.state === 'running' && app.icon ? (
|
||||||
<span style={{ fontSize: sidebarCollapsed ? 18 : 14 }}>{app.icon}</span>
|
<span style={{ fontSize: 14 }}>{app.icon}</span>
|
||||||
) : (
|
) : (
|
||||||
<StatusDot state={app.state} />
|
<StatusDot state={app.state} />
|
||||||
)}
|
)}
|
||||||
{!sidebarCollapsed && app.name}
|
{app.name}
|
||||||
</AppItem>
|
</AppItem>
|
||||||
))}
|
))}
|
||||||
</AppList>
|
</AppList>
|
||||||
{!sidebarCollapsed && (
|
<SidebarFooter>
|
||||||
<SidebarFooter>
|
<NewAppButton>+ New App</NewAppButton>
|
||||||
<NewAppButton>+ New App</NewAppButton>
|
</SidebarFooter>
|
||||||
</SidebarFooter>
|
|
||||||
)}
|
|
||||||
</Sidebar>
|
</Sidebar>
|
||||||
<Main>
|
<Main>
|
||||||
{selected ? (
|
{selected ? (
|
||||||
|
|
|
||||||
|
|
@ -196,7 +196,6 @@ export const stopApp = (dir: string) => {
|
||||||
const watchAppsDir = () => {
|
const watchAppsDir = () => {
|
||||||
watch(APPS_DIR, { recursive: true }, (_event, filename) => {
|
watch(APPS_DIR, { recursive: true }, (_event, filename) => {
|
||||||
if (!filename) return
|
if (!filename) return
|
||||||
if (!filename.includes('/')) return
|
|
||||||
|
|
||||||
// Extract the app directory name from the path (e.g., "myapp/package.json" -> "myapp")
|
// Extract the app directory name from the path (e.g., "myapp/package.json" -> "myapp")
|
||||||
const dir = filename.split('/')[0]!
|
const dir = filename.split('/')[0]!
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user