Compare commits

..

6 Commits

3 changed files with 153 additions and 51 deletions

View File

@ -96,11 +96,24 @@ const RepoName = define('RepoName', {
// Interfaces
// ---------------------------------------------------------------------------
interface AppRepoProps {
appName: string
baseUrl: string
branch: string
exists: boolean
commits: boolean
}
interface LayoutProps {
title: string
children: Child
}
interface RepoListPageProps {
baseUrl: string
repos: Array<{ name: string; commits: boolean; branch: string }>
}
// ---------------------------------------------------------------------------
// Functions
// ---------------------------------------------------------------------------
@ -400,6 +413,123 @@ async function withDeployLock<T>(repo: string, fn: () => Promise<T>): Promise<T>
}
}
function AppRepo({ appName, baseUrl, branch, exists, commits }: AppRepoProps) {
return (
<Layout title={`Git - ${appName}`}>
{exists && commits ? (
<>
<Heading>Repository</Heading>
<RepoList>
<RepoItem>
<div>
<RepoName>{appName}</RepoName>
<HelpText style="margin: 4px 0 0; font-size: 12px">
git clone {baseUrl}/{appName}
</HelpText>
</div>
<div style="display: flex; gap: 8px; align-items: center">
<Badge>{branch}</Badge>
<Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
</div>
</RepoItem>
</RepoList>
<Heading>Push Changes</Heading>
<CodeBlock>{[
`git push toes ${branch}`,
'',
'# Or if remote not yet added:',
`git remote add toes ${baseUrl}/${appName}`,
`git push toes ${branch}`,
].join('\n')}</CodeBlock>
</>
) : exists ? (
<>
<Heading>Repository</Heading>
<RepoList>
<RepoItem>
<div>
<RepoName>{appName}</RepoName>
<HelpText style="margin: 4px 0 0; font-size: 12px">
git clone {baseUrl}/{appName}
</HelpText>
</div>
<Badge>empty</Badge>
</RepoItem>
</RepoList>
<Heading>Push to Deploy</Heading>
<CodeBlock>{[
`git remote add toes ${baseUrl}/${appName}`,
'git push toes main',
].join('\n')}</CodeBlock>
</>
) : (
<>
<Heading>Push to Deploy</Heading>
<HelpText>
No git repository for <strong>{appName}</strong> yet.
Push to create one and deploy.
</HelpText>
<CodeBlock>{[
`git remote add toes ${baseUrl}/${appName}`,
'git push toes main',
].join('\n')}</CodeBlock>
</>
)}
</Layout>
)
}
function RepoListPage({ baseUrl, repos }: RepoListPageProps) {
return (
<Layout title="Git">
<Heading>Push to Deploy</Heading>
<HelpText>
Push a git repository to deploy it as a toes app.
The repo must contain a <code>package.json</code> with a <code>scripts.toes</code> entry.
</HelpText>
<CodeBlock>{[
'# Add this server as a remote and push',
`git remote add toes ${baseUrl}/<app-name>`,
'git push toes main',
'',
'# Or push an existing repo',
`git push ${baseUrl}/<app-name> main`,
].join('\n')}</CodeBlock>
{repos.length > 0 && (
<>
<Heading>Repositories</Heading>
<RepoList>
{repos.map(({ name, commits, branch }) => (
<RepoItem>
<div>
<RepoName>{name}</RepoName>
<HelpText style="margin: 4px 0 0; font-size: 12px">
git clone {baseUrl}/{name}
</HelpText>
</div>
<div style="display: flex; gap: 8px; align-items: center">
<Badge>{branch}</Badge>
{commits
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
: <Badge>empty</Badge>}
</div>
</RepoItem>
))}
</RepoList>
</>
)}
{repos.length === 0 && (
<HelpText>No repositories yet. Push one to get started.</HelpText>
)}
</Layout>
)
}
// ---------------------------------------------------------------------------
// Module init
// ---------------------------------------------------------------------------
@ -503,63 +633,30 @@ app.on('POST', ['/:repo{.+\\.git}/git-receive-pack', '/:repo/git-receive-pack'],
})
app.get('/', async c => {
const repos = await listRepos()
const appName = c.req.query('app')
const host = c.req.header('host') ?? 'git.toes.local'
const baseUrl = `http://${host}`
// When viewing a specific app, only show that app's repo
if (appName) {
const bare = repoPath(appName)
const exists = await dirExists(bare)
const [commits, branch] = exists
? await Promise.all([hasCommits(bare), getDefaultBranch(bare)])
: [false, 'main']
return c.html(<AppRepo appName={appName} baseUrl={baseUrl} branch={branch} exists={exists} commits={commits} />)
}
// No app selected — show all repos
const repos = await listRepos()
const repoData = await Promise.all(repos.map(async name => {
const bare = repoPath(name)
const [commits, branch] = await Promise.all([hasCommits(bare), getDefaultBranch(bare)])
return { name, commits, branch }
}))
return c.html(
<Layout title="Git">
<Heading>Push to Deploy</Heading>
<HelpText>
Push a git repository to deploy it as a toes app.
The repo must contain a <code>package.json</code> with a <code>scripts.toes</code> entry.
</HelpText>
<CodeBlock>{[
'# Add this server as a remote and push',
`git remote add toes ${baseUrl}/<app-name>.git`,
'git push toes main',
'',
'# Or push an existing repo',
`git push ${baseUrl}/<app-name>.git main`,
].join('\n')}</CodeBlock>
{repoData.length > 0 && (
<>
<Heading>Repositories</Heading>
<RepoList>
{repoData.map(({ name, commits, branch }) => (
<RepoItem>
<div>
<RepoName>{name}</RepoName>
<HelpText style="margin: 4px 0 0; font-size: 12px">
git clone {baseUrl}/{name}.git
</HelpText>
</div>
<div style="display: flex; gap: 8px; align-items: center">
<Badge>{branch}</Badge>
{commits
? <Badge style={`color: ${theme('colors-statusRunning')}`}>deployed</Badge>
: <Badge>empty</Badge>}
</div>
</RepoItem>
))}
</RepoList>
</>
)}
{repoData.length === 0 && (
<HelpText>No repositories yet. Push one to get started.</HelpText>
)}
</Layout>,
)
return c.html(<RepoListPage baseUrl={baseUrl} repos={repoData} />)
})
export default app.defaults

View File

@ -24,13 +24,14 @@ function convert(app: BackendApp): SharedApp {
// SSE: full app state snapshots for the dashboard UI (every state change)
// For discrete lifecycle events consumed by app processes, see /api/events/stream
router.sse('/stream', (send) => {
let queue = Promise.resolve()
const broadcast = () => {
const apps: SharedApp[] = allApps().map(({
name, state, icon, error, port, started, logs, tool, tunnelEnabled, tunnelUrl
}) => ({
name, state, icon, error, port, started, logs, tool, tunnelEnabled, tunnelUrl,
}))
send(apps)
queue = queue.then(() => send(apps))
}
broadcast()

View File

@ -7,8 +7,12 @@ const router = Hype.router()
// Unlike /api/apps/stream (full state snapshots for the dashboard), this sends
// individual events so apps can react to specific lifecycle changes.
router.sse('/stream', (send) => {
const unsub = onEvent(event => send(event))
const heartbeat = setInterval(() => send('', 'ping'), 60_000)
let queue = Promise.resolve()
const safeSend = (...args: Parameters<typeof send>) => {
queue = queue.then(() => send(...args))
}
const unsub = onEvent(event => safeSend(event))
const heartbeat = setInterval(() => safeSend('', 'ping'), 60_000)
return () => {
clearInterval(heartbeat)
unsub()