OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-deploy / examples / web.js

133 lines · javascript ref:
#!/usr/bin/env node
/**
 * Mini web UI example for hyper-deploy
 *
 * Usage:
 *   node examples/web.js <dir> [--port 9090] [--topic <hex>]
 *
 * Starts a deploy server and an HTTP dashboard showing live status.
 */

import http from 'http'
import { DeployServer } from '../index.js'

const args = process.argv.slice(2)
const dir = args[0]
if (!dir) { console.error('Usage: web.js <dir> [--port N]'); process.exit(1) }

let port = 9090
let topic
for (let i = 1; i < args.length; i++) {
  if (args[i] === '--port' && args[i + 1]) port = parseInt(args[++i])
  if (args[i] === '--topic' && args[i + 1]) topic = Buffer.from(args[++i], 'hex')
}

function fmt (b) {
  if (b < 1024) return b + ' B'
  if (b < 1048576) return (b / 1024).toFixed(1) + ' KB'
  if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB'
  return (b / 1073741824).toFixed(2) + ' GB'
}

const server = new DeployServer(dir, { topic })
const sseClients = new Set()

function broadcast (event, data) {
  const msg = 'event: ' + event + '\ndata: ' + JSON.stringify(data) + '\n\n'
  for (const res of sseClients) {
    try { res.write(msg) } catch { sseClients.delete(res) }
  }
}

server.on('peer', (info) => broadcast('peer', info))
server.on('progress', (info) => broadcast('progress', info))

function getStatus () {
  return {
    dir: server.dir,
    topic: server.topicHex,
    files: server.manifest ? server.manifest.files.length : 0,
    totalSize: server.manifest ? fmt(server.manifest.totalSize) : '0',
    connections: server.connections,
    filesServed: server.stats.filesServed,
    bytesServed: fmt(server.stats.bytesServed)
  }
}

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(getStatus()))
    return
  }

  if (url === '/events') {
    res.writeHead(200, {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive'
    })
    res.write('data: ' + JSON.stringify(getStatus()) + '\n\n')
    sseClients.add(res)
    req.on('close', () => sseClients.delete(res))
    return
  }

  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' })
  res.end(`<!DOCTYPE html>
<html><head><title>hyper-deploy</title>
<style>
  body { font-family: monospace; background: #0d1117; color: #c9d1d9; padding: 2rem; max-width: 640px; margin: 0 auto; }
  h1 { color: #58a6ff; }
  .card { background: #161b22; border: 1px solid #30363d; border-radius: 6px; padding: 1rem; margin: 1rem 0; }
  .row { display: flex; justify-content: space-between; padding: 0.3rem 0; }
  .label { color: #8b949e; }
  .topic { font-size: 0.75rem; word-break: break-all; color: #79c0ff; }
  #log { max-height: 300px; overflow-y: auto; font-size: 0.8rem; color: #8b949e; }
  #log div { padding: 2px 0; border-bottom: 1px solid #21262d; }
</style></head><body>
<h1>hyper-deploy</h1>
<div class="card">
  <div class="topic" id="topic">...</div>
</div>
<div class="card">
  <div class="row"><span class="label">Files</span><span id="files">-</span></div>
  <div class="row"><span class="label">Size</span><span id="size">-</span></div>
  <div class="row"><span class="label">Peers</span><span id="peers">0</span></div>
  <div class="row"><span class="label">Served</span><span id="served">0 files</span></div>
  <div class="row"><span class="label">Bytes</span><span id="bytes">0 B</span></div>
</div>
<div class="card"><h3 style="color:#8b949e;margin:0 0 0.5rem">Log</h3><div id="log"></div></div>
<script>
const $ = id => document.getElementById(id)
function log(msg) {
  const d = document.createElement('div'); d.textContent = msg
  $('log').prepend(d)
  while ($('log').children.length > 100) $('log').lastChild.remove()
}
function update(d) {
  if (d.topic) $('topic').textContent = d.topic
  if (d.files !== undefined) $('files').textContent = d.files
  if (d.totalSize) $('size').textContent = d.totalSize
  if (d.connections !== undefined) $('peers').textContent = d.connections
  if (d.filesServed !== undefined) $('served').textContent = d.filesServed + ' files'
  if (d.bytesServed) $('bytes').textContent = d.bytesServed
}
const es = new EventSource('/events')
es.onmessage = e => update(JSON.parse(e.data))
es.addEventListener('peer', e => { const d = JSON.parse(e.data); log('Peer ' + d.key + ': ' + d.state) })
es.addEventListener('progress', e => { const d = JSON.parse(e.data); if (d.file) log('sent ' + d.file) })
</script></body></html>`)
})

server.ready().then(() => {
  httpServer.listen(port, () => {
    console.log('Serving: %s', server.dir)
    console.log('Topic:   %s', server.topicHex)
    console.log('Files:   %d (%s)', server.manifest.files.length, fmt(server.manifest.totalSize))
    console.log('Web UI:  http://localhost:%d', port)
  })
}).catch((err) => { console.error(err.message); process.exit(1) })