OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-sync / examples / web.js

152 lines · javascript ref:
#!/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) })