#!/usr/bin/env node
/**
* Mini web UI example for hyper-sync
*
* Usage:
* node examples/web.js [--port 9091] [--repos <dir>]
*
* Starts a sync server and HTTP dashboard showing repo status.
*/
const http = require('http')
const HyperSync = require('../index')
const args = process.argv.slice(2)
function getFlag (name) {
const i = args.indexOf('--' + name)
return i !== -1 && args[i + 1] ? args[i + 1] : null
}
function fmt (b) {
if (b < 1024) return b + ' B'
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
return (b / 1048576).toFixed(1) + ' MB'
}
const port = parseInt(getFlag('port')) || 9091
const reposDir = getFlag('repos')
const sync = new HyperSync(reposDir ? { repos: reposDir } : {})
const events = []
function pushEvent (msg) {
events.push({ t: Date.now(), msg })
if (events.length > 200) events.shift()
}
sync.on('serve', (name) => pushEvent('serving ' + name))
sync.on('connection', (peer, repo) => pushEvent('peer ' + peer + ' → ' + (repo || '?')))
sync.on('pull', (remote, name, state) => pushEvent('pull ' + remote + '/' + name + ': ' + state))
const httpServer = http.createServer((req, res) => {
const url = req.url.split('?')[0]
if (url === '/api/status') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(sync.status()))
return
}
if (url === '/api/rescan' && req.method === 'POST') {
sync.scan().then(() => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(sync.status()))
}).catch((err) => {
res.writeHead(500, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: err.message }))
})
return
}
if (url === '/api/events') {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(events.slice(-50)))
return
}
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
res.end(renderPage())
})
function renderPage () {
return `<!DOCTYPE html>
<html><head><title>hyper-sync</title>
<style>
body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 720px; margin: 0 auto; }
h1 { color: #58a6ff; }
.card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
.card h2 { font-size: 0.75rem; text-transform: uppercase; color: #8b949e; margin: 0 0 0.5rem; }
.key { font-size: 0.75rem; word-break: break-all; color: #79c0ff; }
.row { display: flex; justify-content: space-between; padding: 0.3rem 0; border-bottom: 1px solid #21262d; }
.row:last-child { border-bottom: none; }
.label { color: #8b949e; }
table { width: 100%; border-collapse: collapse; font-size: 0.85rem; }
th { text-align: left; color: #8b949e; padding: 0.3rem 0.5rem; border-bottom: 1px solid #30363d; }
td { padding: 0.3rem 0.5rem; border-bottom: 1px solid #21262d; }
.serving { color: #3fb950; }
.btn { background: #21262d; color: #c9d1d9; border: 1px solid #30363d; padding: 0.3rem 0.6rem; border-radius: 4px; cursor: pointer; font-size: 0.8rem; }
.btn:hover { background: #30363d; }
#log { max-height: 200px; overflow-y: auto; font-size: 0.8rem; color: #8b949e; }
#log div { padding: 2px 0; }
</style></head><body>
<h1>hyper-sync</h1>
<div class="card">
<h2>Server <button class="btn" onclick="rescan()">Rescan</button></h2>
<div class="key" id="key">...</div>
<div class="row" style="margin-top:0.5rem"><span class="label">Repos</span><span id="count">-</span></div>
<div class="row"><span class="label">Serving</span><span id="serving">-</span></div>
</div>
<div class="card">
<h2>Repositories</h2>
<table><thead><tr><th>Name</th><th>Branches</th><th>Size</th><th>HEAD</th><th>Status</th></tr></thead>
<tbody id="repos"></tbody></table>
</div>
<div class="card"><h2>Events</h2><div id="log"></div></div>
<script>
const $ = id => document.getElementById(id)
function fmt(b) {
if (b < 1024) return b + ' B'
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
return (b / 1048576).toFixed(1) + ' MB'
}
function refresh() {
fetch('/api/status').then(r => r.json()).then(d => {
$('key').textContent = d.serverKey || '(none)'
$('count').textContent = d.repoCount
$('serving').textContent = d.serving
$('repos').innerHTML = d.repos.map(r =>
'<tr><td>' + r.name + '</td><td>' + r.branches + '</td><td>' + fmt(r.packSize) +
'</td><td>' + (r.head || '-') + '</td><td>' +
(r.serving ? '<span class="serving">serving</span>' : '-') + '</td></tr>'
).join('')
})
fetch('/api/events').then(r => r.json()).then(evts => {
$('log').innerHTML = evts.reverse().map(e =>
'<div>' + new Date(e.t).toLocaleTimeString() + ' ' + e.msg + '</div>'
).join('')
})
}
function rescan() { fetch('/api/rescan', { method: 'POST' }).then(refresh) }
refresh()
setInterval(refresh, 5000)
</script></body></html>`
}
sync.ready().then(async () => {
await sync.serve()
httpServer.listen(port, () => {
console.log('Server key: %s', sync.serverKey)
console.log('Repos: %d', sync.repos.size)
console.log('Serving: %d topics', sync.serving)
console.log('Web UI: http://localhost:%d', port)
})
}).catch((err) => { console.error(err.message); process.exit(1) })