Compare commits

...

7 Commits

7 changed files with 89 additions and 27 deletions

View File

@ -44,7 +44,7 @@ async function buildTarget(target: BuildTarget) {
ENTRY_POINT, ENTRY_POINT,
'--compile', '--compile',
'--target', '--target',
'bun', `bun-${target.os}-${target.arch}`,
'--minify', '--minify',
'--sourcemap=external', '--sourcemap=external',
'--outfile', '--outfile',
@ -52,11 +52,6 @@ async function buildTarget(target: BuildTarget) {
], { ], {
stdout: 'inherit', stdout: 'inherit',
stderr: 'inherit', stderr: 'inherit',
env: {
...process.env,
BUN_TARGET_OS: target.os,
BUN_TARGET_ARCH: target.arch,
},
}) })
const exitCode = await proc.exited const exitCode = await proc.exited

View File

@ -17,12 +17,13 @@ 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, collapsed, switcherStyle, listStyle }: AppSelectorProps) { export function AppSelector({ render, onSelect, onDashboard, collapsed, switcherStyle, listStyle }: AppSelectorProps) {
const selectApp = (name: string) => { const selectApp = (name: string) => {
setSelectedApp(name) setSelectedApp(name)
onSelect?.() onSelect?.()
@ -51,6 +52,16 @@ export function AppSelector({ render, onSelect, collapsed, switcherStyle, listSt
</SectionSwitcher> </SectionSwitcher>
)} )}
<AppList style={listStyle}> <AppList style={listStyle}>
{collapsed && onDashboard && (
<AppItem
onClick={onDashboard}
selected={!selectedApp ? true : undefined}
style={{ justifyContent: 'center', padding: '10px 12px' }}
title="Toes"
>
<span style={{ fontSize: 18 }}>🐾</span>
</AppItem>
)}
{activeApps.map(app => ( {activeApps.map(app => (
<AppItem <AppItem
key={app.name} key={app.name}

View File

@ -5,7 +5,6 @@ import {
AppSelectorChevron, AppSelectorChevron,
DashboardContainer, DashboardContainer,
DashboardHeader, DashboardHeader,
DashboardInstallCmd,
DashboardTitle, DashboardTitle,
SettingsGear, SettingsGear,
StatusDot, StatusDot,
@ -50,9 +49,6 @@ export function DashboardLanding({ render }: { render: () => void }) {
</AppSelectorChevron> </AppSelectorChevron>
)} )}
</DashboardTitle> </DashboardTitle>
<DashboardInstallCmd>
curl -fsSL {location.origin}/install | bash
</DashboardInstallCmd>
</DashboardHeader> </DashboardHeader>
<StatusDotsRow> <StatusDotsRow>

View File

@ -3,6 +3,7 @@ import { getWifiConfig, saveWifiConfig } from '../api'
import { setCurrentView } from '../state' import { setCurrentView } from '../state'
import { import {
Button, Button,
DashboardInstallCmd,
FormActions, FormActions,
FormField, FormField,
FormInput, FormInput,
@ -45,13 +46,19 @@ export function SettingsPage({ render }: { render: () => void }) {
return ( return (
<Main> <Main>
<MainHeader> <MainHeader centered>
<MainTitle>Settings</MainTitle> <MainTitle>Settings</MainTitle>
<HeaderActions> <HeaderActions>
<Button onClick={goBack}>Back</Button> <Button onClick={goBack}>Back</Button>
</HeaderActions> </HeaderActions>
</MainHeader> </MainHeader>
<MainContent> <MainContent centered>
<Section>
<SectionTitle>Install CLI</SectionTitle>
<DashboardInstallCmd>
curl -fsSL {location.origin}/install | bash
</DashboardInstallCmd>
</Section>
<Section> <Section>
<SectionTitle>WiFi</SectionTitle> <SectionTitle>WiFi</SectionTitle>
<form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 400 }}> <form onSubmit={handleSave} style={{ display: 'flex', flexDirection: 'column', gap: 16, maxWidth: 400 }}>

View File

@ -30,17 +30,27 @@ export function Sidebar({ render }: { render: () => void }) {
return ( return (
<SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}> <SidebarContainer style={sidebarCollapsed ? { width: 'auto' } : undefined}>
<Logo> {sidebarCollapsed ? (
<LogoLink onClick={goToDashboard} title="Go to dashboard"> <div style={{ display: 'flex', justifyContent: 'center', padding: '12px 0' }}>
{sidebarCollapsed ? '🐾' : '🐾 Toes'} <HamburgerButton onClick={toggleSidebar} title="Show sidebar">
</LogoLink> <HamburgerLine />
<HamburgerButton onClick={toggleSidebar} title={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}> <HamburgerLine />
<HamburgerLine /> <HamburgerLine />
<HamburgerLine /> </HamburgerButton>
<HamburgerLine /> </div>
</HamburgerButton> ) : (
</Logo> <Logo>
<AppSelector render={render} collapsed={sidebarCollapsed} /> <LogoLink onClick={goToDashboard} title="Go to dashboard">
🐾 Toes
</LogoLink>
<HamburgerButton onClick={toggleSidebar} title="Hide sidebar">
<HamburgerLine />
<HamburgerLine />
<HamburgerLine />
</HamburgerButton>
</Logo>
)}
<AppSelector render={render} collapsed={sidebarCollapsed} onDashboard={goToDashboard} />
{!sidebarCollapsed && ( {!sidebarCollapsed && (
<SidebarFooter> <SidebarFooter>
<NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton> <NewAppButton onClick={openNewAppModal}>+ New App</NewAppButton>

View File

@ -148,6 +148,13 @@ export const MainHeader = define('MainHeader', {
justifyContent: 'space-between', justifyContent: 'space-between',
padding: '0 24px', padding: '0 24px',
borderBottom: `1px solid ${theme('colors-border')}`, borderBottom: `1px solid ${theme('colors-border')}`,
variants: {
centered: {
maxWidth: 560,
margin: '0 auto',
width: '100%',
},
},
}) })
export const MainTitle = define('MainTitle', { export const MainTitle = define('MainTitle', {
@ -196,6 +203,13 @@ export const MainContent = define('MainContent', {
flexDirection: 'column', flexDirection: 'column',
padding: '10px 24px', padding: '10px 24px',
overflow: 'hidden', overflow: 'hidden',
variants: {
centered: {
maxWidth: 560,
margin: '0 auto',
width: '100%',
},
},
}) })
export const DashboardContainer = define('DashboardContainer', { export const DashboardContainer = define('DashboardContainer', {

View File

@ -59,9 +59,36 @@ app.all('/api/tools/:tool/:path{.+}', async c => {
}) })
}) })
const BUILD_SCRIPT = import.meta.dir + '/../../scripts/build.ts'
const DIST_DIR = import.meta.dir + '/../../dist' const DIST_DIR = import.meta.dir + '/../../dist'
const INSTALL_SCRIPT = await Bun.file(import.meta.dir + '/install.sh').text() const INSTALL_SCRIPT = await Bun.file(import.meta.dir + '/install.sh').text()
const BUILD_TARGETS = [
'toes-macos-arm64',
'toes-macos-x64',
'toes-linux-arm64',
'toes-linux-x64',
]
const buildInFlight = new Map<string, Promise<boolean>>()
async function buildBinary(name: string): Promise<boolean> {
const existing = buildInFlight.get(name)
if (existing) return existing
const promise = (async () => {
const proc = Bun.spawn(
['bun', 'run', BUILD_SCRIPT, `--target=${name}`],
{ stdout: 'inherit', stderr: 'inherit' },
)
return (await proc.exited) === 0
})()
buildInFlight.set(name, promise)
promise.finally(() => buildInFlight.delete(name))
return promise
}
// Install script: curl -fsSL http://toes.local/install | bash // Install script: curl -fsSL http://toes.local/install | bash
app.get('/install', c => { app.get('/install', c => {
if (!TOES_URL) return c.text('TOES_URL is not configured', 500) if (!TOES_URL) return c.text('TOES_URL is not configured', 500)
@ -69,7 +96,7 @@ app.get('/install', c => {
return c.text(script, 200, { 'content-type': 'text/plain' }) return c.text(script, 200, { 'content-type': 'text/plain' })
}) })
// Serve built CLI binaries from dist/ // Serve built CLI binaries from dist/, building on-demand if needed
app.get('/dist/:file', async c => { app.get('/dist/:file', async c => {
const file = c.req.param('file') const file = c.req.param('file')
if (!file || file.includes('/') || file.includes('..')) { if (!file || file.includes('/') || file.includes('..')) {
@ -77,9 +104,11 @@ app.get('/dist/:file', async c => {
} }
const bunFile = Bun.file(`${DIST_DIR}/${file}`) const bunFile = Bun.file(`${DIST_DIR}/${file}`)
if (!(await bunFile.exists())) { if (!(await bunFile.exists())) {
return c.text(`Binary "${file}" not found — run cli:build:all on the server`, 404) if (!BUILD_TARGETS.includes(file)) return c.text('Not found', 404)
const ok = await buildBinary(file)
if (!ok) return c.text(`Failed to build "${file}"`, 500)
} }
return new Response(bunFile, { return new Response(Bun.file(`${DIST_DIR}/${file}`), {
headers: { 'content-type': 'application/octet-stream' }, headers: { 'content-type': 'application/octet-stream' },
}) })
}) })