Rudder learns whether this server should have an engine even when its event stream is buffered; the legend bar keeps its rounded ends

The dev backend answers /status with the same offline status as /events, and
Rudder reads it on mount and whenever the event stream drops, so a proxy that
buffers the stream no longer leaves it saying "Runs on your computer" on a
server that should run the engine. The local-microphone notice no longer shows
off this computer. The legend progress bar paints its grey band as a rounded
strip and rounds the first and last segment, which the 24px touch targets had
squared off.
This commit is contained in:
Doxa Maps 2026-09-27 22:53:04 +00:00
parent 8e267b0ce5
commit 55573df1d2
3 changed files with 23 additions and 4 deletions

View file

@ -88,12 +88,18 @@ export function backendDevPlugin(root) {
for (const b of DEV_BACKENDS) {
server.middlewares.use(b.route, (req, res, next) => {
if (isAlive(b.name)) return next()
const status = { engine: 'offline', expected: !!(b.expected && b.expected()), error: 'The desktop engine is not running.' }
if ((req.url || '').startsWith('/events')) {
res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' })
const expected = !!(b.expected && b.expected())
res.write(`data: ${JSON.stringify({ type: 'status', status: { engine: 'offline', expected, error: 'The desktop engine is not running.' } })}\n\n`)
res.write(`data: ${JSON.stringify({ type: 'status', status })}\n\n`)
return // held open, so the EventSource does not reconnect in a loop
}
// /status answers the same, for a page whose event stream cannot get through (a proxy that buffers).
if ((req.url || '').startsWith('/status')) {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(status))
return
}
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end('[]')
})

View file

@ -71,7 +71,13 @@ useShadowStyles(`
tall so each segment button is a 24px target, but it paints only a 10px band: the track's background is
a centred 10px strip and each segment clips its colour to its content box inside 7px of padding. The
-7px margins keep the legend's layout exactly 10px tall, as before. */
.lpb-track{position:relative;flex:1 1 auto;min-width:60px;display:flex;height:24px;margin:-7px 0;overflow:hidden;background:linear-gradient(rgba(110,118,129,0.25),rgba(110,118,129,0.25)) center/100% 10px no-repeat;}
.lpb-track{position:relative;flex:1 1 auto;min-width:60px;display:flex;height:24px;margin:-7px 0;overflow:hidden;}
/* The grey band, rounded, under the segments; the first and last segment round the bar's two ends (an
elliptical radius 5px/12px leaves a 5px curve on the 10px painted band inside the 7px padding). */
.lpb-track::before{content:'';position:absolute;left:0;right:0;top:7px;height:10px;border-radius:5px;background:rgba(110,118,129,0.25);}
.lpb-seg{position:relative;}
.lpb-span:first-child .lpb-seg:first-child{border-top-left-radius:5px 12px;border-bottom-left-radius:5px 12px;}
.lpb-seg-rest,.lpb-span:last-child .lpb-seg:last-child{border-top-right-radius:5px 12px;border-bottom-right-radius:5px 12px;}
.lpb-span{display:flex;height:100%;min-width:0;flex:0 0 auto;}
.lpb-seg{appearance:none;-webkit-appearance:none;border:none;margin:0;padding:7px 0;box-sizing:border-box;background-clip:content-box !important;height:100%;display:block;flex:0 0 auto;cursor:pointer;outline:none;-webkit-tap-highlight-color:transparent;transition:filter 0.12s,box-shadow 0.12s;}
.lpb-seg-rest{flex:1 1 auto;}

View file

@ -308,6 +308,7 @@ function connect() {
engine.value = 'offline'
if (audioMode.value !== 'browser') recording.value = false
logc('events disconnected; retrying in 3s')
fetchStatus()
try { es.close() } catch {}
retry = setTimeout(connect, 3000)
}
@ -544,7 +545,13 @@ function onKey(ev) {
if (ev.ctrlKey && ev.shiftKey && ev.code === 'Space') { ev.preventDefault(); toggle() }
}
// The status also by plain request: a proxy that buffers the event stream (rega's harness did) must not
// leave the page guessing whether this server is meant to have an engine.
async function fetchStatus() {
try { const r = await fetch(apiBase.value + '/status'); if (r.ok) applyStatus(await r.json()) } catch {}
}
onMounted(() => {
fetchStatus()
connect()
refreshDevices()
window.addEventListener('keydown', onKey)
@ -675,7 +682,7 @@ const statusText = computed(() => ({
</p>
<p v-if="chosen && chosen.muted" class="notice" data-kind="warn">“{{ chosen.label }}” is muted in your system sound settings — Rudder won't hear anything until it's unmuted.</p>
<p v-if="deviceError && audioMode !== 'browser'" class="notice" data-kind="error">{{ deviceError }}</p>
<p v-else-if="!devices.length && engine !== 'offline' && audioMode !== 'browser'" class="notice" data-kind="warn">No microphone found. Connect a headset or turn on your computer's microphone in Sound settings.</p>
<p v-else-if="!devices.length && engine !== 'offline' && audioMode !== 'browser' && onOwnComputer" class="notice" data-kind="warn">No microphone found. Connect a headset or turn on your computer's microphone in Sound settings.</p>
<p v-if="offCards.length" class="hint">Also available but switched off in Sound settings: {{ offCards.join(', ') }}. Turn it on there to use it here.</p>
<p v-if="notice" class="notice" :data-kind="noticeKind">{{ notice }}</p>