{% extends "base.html" %}
{% block content %}
<style>
.live-indicator {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 500;
color: var(--text-subtle);
margin-bottom: 16px;
user-select: none;
}
.live-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: var(--text-subtle);
flex-shrink: 0;
transition: background 300ms ease;
}
.live-dot.connected {
background: var(--status-done);
animation: pulse-dot 2.5s ease-in-out infinite;
}
.live-dot.error {
background: var(--critical);
}
@keyframes pulse-dot {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
ul.card-list {
list-style: none;
padding: 0;
margin: 0;
}
ul.card-list li {
list-style: none;
padding: 0;
margin: 0;
}
</style>
<div class="live-indicator">
<span class="live-dot" id="live-dot"></span>
<span id="live-label">Connecting...</span>
</div>
<div class="board" id="board">
{% for col in columns %}
<div class="column" data-status="{{ col.status }}">
<div class="column-header">
{{ col.label }}
<span class="count">{{ col.issues | length }}</span>
</div>
<ul class="card-list" role="list">
{% for issue in col.issues %}
<li class="card-item">
<div class="card">
<div class="card-id">BMO-{{ issue.id }}</div>
<div class="card-title"><a href="/issues/{{ issue.id }}">{{ issue.title }}</a></div>
<div class="card-meta">
<span class="badge priority-{{ issue.priority }}">{{ issue.priority }}</span>
<span class="badge kind-{{ issue.kind }}">{{ issue.kind }}</span>
</div>
</div>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
<script>
(function () {
'use strict';
var dot = document.getElementById('live-dot');
var label = document.getElementById('live-label');
var board = document.getElementById('board');
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function buildCard(issue) {
return [
'<div class="card">',
' <div class="card-id">BMO-' + escapeHtml(issue.id) + '</div>',
' <div class="card-title"><a href="/issues/' + escapeHtml(issue.id) + '">' + escapeHtml(issue.title) + '</a></div>',
' <div class="card-meta">',
' <span class="badge priority-' + escapeHtml(issue.priority) + '">' + escapeHtml(issue.priority) + '</span>',
' <span class="badge kind-' + escapeHtml(issue.kind) + '">' + escapeHtml(issue.kind) + '</span>',
' </div>',
'</div>'
].join('\n');
}
function buildColumn(col) {
var issueCount = col.issues ? col.issues.length : 0;
var listItems = issueCount > 0
? col.issues.map(function (issue) {
return '<li class="card-item">\n' + buildCard(issue) + '\n</li>';
}).join('\n')
: '';
var cardList = '<ul class="card-list" role="list">\n' + listItems + '\n</ul>';
return [
'<div class="column" data-status="' + escapeHtml(col.status) + '">',
' <div class="column-header">',
' ' + escapeHtml(col.label),
' <span class="count">' + issueCount + '</span>',
' </div>',
cardList,
'</div>'
].join('\n');
}
function refreshBoard() {
fetch('/api/board')
.then(function (res) {
if (!res.ok) { throw new Error('HTTP ' + res.status); }
return res.json();
})
.then(function (data) {
var columns = data && data.data && data.data.columns;
if (!Array.isArray(columns)) { return; }
var rendered = {};
columns.forEach(function (col) {
var existing = Array.from(board.children).find(function (el) { return el.dataset.status === col.status; });
var html = buildColumn(col);
var tmp = document.createElement('div');
tmp.innerHTML = html;
var newCol = tmp.firstElementChild;
if (existing) {
board.replaceChild(newCol, existing);
} else {
board.appendChild(newCol);
}
rendered[col.status] = true;
});
Array.prototype.slice.call(board.querySelectorAll('[data-status]')).forEach(function (el) {
if (!rendered[el.getAttribute('data-status')]) {
board.removeChild(el);
}
});
})
.catch(function (err) {
console.warn('[bmo] board refresh failed:', err);
});
}
function setStatus(state) {
dot.className = 'live-dot ' + state;
if (state === 'connected') {
label.textContent = 'Live';
} else if (state === 'error') {
label.textContent = 'Reconnecting...';
} else {
label.textContent = 'Connecting...';
}
}
function connect() {
var es = new EventSource('/api/events');
es.addEventListener('open', function () {
setStatus('connected');
});
es.addEventListener('board_updated', function () {
refreshBoard();
});
es.addEventListener('message', function (evt) {
try {
var payload = JSON.parse(evt.data);
if (payload && payload.type === 'board_updated') {
refreshBoard();
}
} catch (_) {
}
});
es.addEventListener('error', function () {
setStatus('error');
if (es.readyState === EventSource.CLOSED) {
es.close();
setTimeout(connect, 3000);
}
});
}
connect();
}());
</script>
{% endblock %}