3 SSE
fmatte edited this page 2025-09-02 01:34:29 -04:00

./server/index.js

import command from './admin/command/command.js'

server.use(servu.get('/admin/command/', redirect('/admin/command')))
server.use(servu.get('/admin/command', command.index))
server.use(servu.get('/admin/command/run/:id', command.run))

./admin/command/command.js

import { nameChanged } from '#comico/commands/healthNameChanged'
import { infoCount } from '#comico/commands/infoCount'
import { importFromLibraries } from '#comico/commands/importFromLibraries'

const commands = {
  1: { name: "Info: Count", func: infoCount },
  2: { name: "Health: Name Changed", func: nameChanged },
  3: { name: "Import", func: importFromLibraries }
}

const index = async (context) => {
  const list = Object.keys(commands).map(id => ({ id, name: commands[id].name }))

  await context.html(import.meta.dirname + '/command.mustache', { list, url: context.req.url }, {})
}

const run = async (context) => {
  const { id } = context.req.params
  const command = commands[id]

  await context.sse(async (log) => {
    log({ event: 'start', data: command.name })
    await comico.command(command.func, (data) => log({ event: 'log', data }))
    log({ event: 'stop', data: command.name })
  })
}

export default { index, run }

./server/context.js

const context = (req, res) => {
  return {
    sse: async (callback) => {
        res.setHeader('Content-Type', 'text/event-stream')
        res.setHeader('Cache-Control', 'no-cache')
        res.setHeader('Connection', 'keep-alive')

        await callback((log) => {
            res.write(`event: ${log.event}\ndata:${log.data}\n\n`)
        })

        res.end()
    }
  }
}

./comico/commands/infoCount.js

const infoCount = async (comico, event = () => { }) => {
  event(comico.comicsRepo.values.length + ' comics')
}

export { infoCount }

./server/admin/command/command.mustache

<style>
  #commands {
    display: flex;
    flex-wrap: wrap;
    gap: 1em;
  }

  button {
    padding: 5px;
  }

  #result {
    margin-block: 1em;
    width: 100%;
    height: 45vh;
  }
</style>
<main id="container">
  <div id="commands">
    {{#list}}
    <button data-id="{{id}}">{{name}}</button>
    {{/list}}
  </div>

  <textarea id="result"></textarea>
</main>

<script type="module">
  const result = document.getElementById('result')
  const commands = document.getElementById('commands')

  const clear = () => {
    result.value = ''
  }

  const add = (msg) => {
    result.value += msg + '\n'
  }

  const disabled = () => {
    commands.querySelectorAll('button').forEach(b => b.disabled = true)
  }

  const enabled = () => {
    commands.querySelectorAll('button').forEach(b => b.disabled = false)
  }

  const commandOnClick = (evt) => {
    const { target } = evt
    const { id } = target.dataset
    disabled()
    clear()

    const url = `{{{url}}}/run/${id}`
    const eventSource = new EventSource(url)

    eventSource.addEventListener('start', (e) => {
      add('Running: ' + e.data)
    })
    eventSource.addEventListener('log', (e) => {
      add(e.data)
    })
    eventSource.addEventListener('stop', (e) => {
      add('DONE')
      eventSource.close()
      enabled()
    })

    eventSource.onerror = function (event) {
      console.log('Error occurred:', event)
      eventSource.close()
      enabled()
    }
  }

  commands.querySelectorAll('button').forEach(button => button.addEventListener('click', commandOnClick))
  clear()
</script>