app/flow.py: séjours par colonne reconstruits du journal,
médianes, colonne la plus lente; zombies = aucune activité
(journal/Gitea) depuis KANBANTE_ZOMBIE_DAYS (30). Pages /health
et /health/{o}/{r} + actions rapides (commenter/backlog/fermer).
185 lines
7.1 KiB
JavaScript
185 lines
7.1 KiB
JavaScript
/**
|
|
* Kanbante — Command palette (Ctrl+K), roadmap #10.
|
|
*
|
|
* One overlay for the whole app: static actions, every project, and
|
|
* global issue search (via /api/palette). Fully keyboard driven.
|
|
*/
|
|
(function () {
|
|
var overlay = document.getElementById('palette-overlay');
|
|
if (!overlay) return;
|
|
var input = document.getElementById('palette-input');
|
|
var list = document.getElementById('palette-results');
|
|
|
|
var ACTIONS = [
|
|
{ icon: '🏠', label: 'Projets — dashboard', href: '/' },
|
|
{ icon: '🎯', label: 'Focus — en cours, QA, révision', href: '/focus' },
|
|
{ icon: '🌅', label: "Focus du jour — les 3 cartes d'aujourd'hui", href: '/today' },
|
|
{ icon: '🔮', label: 'Prévisions — on finit quand ?', href: '/forecast' },
|
|
{ icon: '🗣️', label: 'Standup automatique — hier, aujourd\'hui, blocages', href: '/standup' },
|
|
{ icon: '⚙️', label: 'Règles d\'automatisation', href: '/rules' },
|
|
{ icon: '🩺', label: 'Santé du flux — goulots et zombies', href: '/health' },
|
|
{ icon: '👤', label: 'Mes issues', href: '/my' },
|
|
{ icon: '📰', label: 'Activité — journal des événements', href: '/activity' },
|
|
{ icon: '📊', label: 'Briefing quotidien', href: '/briefing' },
|
|
{ icon: '⚡', label: 'Capture éclair — nouvelle idée', href: '/capture' },
|
|
{ icon: '📥', label: 'Mode Triage — vider un backlog au clavier', href: '/triage' },
|
|
{ icon: '🧠', label: 'Digest hebdo — vers ObsiGate', href: '/digest' },
|
|
{ icon: '🖥️', label: 'Wallboard — mode TV', href: '/wall' },
|
|
{ icon: '🔍', label: 'Recherche avancée', href: '/search' },
|
|
];
|
|
|
|
var projects = null; // cached after first fetch
|
|
var issues = []; // latest search results for the query
|
|
var items = []; // rendered items
|
|
var activeIndex = 0;
|
|
var debounce = null;
|
|
var lastQuery = '';
|
|
|
|
function open() {
|
|
overlay.hidden = false;
|
|
document.body.style.overflow = 'hidden';
|
|
input.value = '';
|
|
issues = [];
|
|
lastQuery = '';
|
|
render('');
|
|
input.focus();
|
|
if (projects === null) {
|
|
fetch('/api/palette')
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
projects = data.projects || [];
|
|
render(input.value.trim());
|
|
})
|
|
.catch(function () { projects = []; });
|
|
}
|
|
}
|
|
|
|
function close() {
|
|
overlay.hidden = true;
|
|
document.body.style.overflow = '';
|
|
}
|
|
|
|
function buildItems(query) {
|
|
var q = query.toLowerCase();
|
|
var out = [];
|
|
ACTIONS.forEach(function (a) {
|
|
if (!q || a.label.toLowerCase().includes(q)) {
|
|
out.push({ icon: a.icon, label: a.label, sub: 'Action', href: a.href });
|
|
}
|
|
});
|
|
(projects || []).forEach(function (p) {
|
|
if (out.length >= 14) return;
|
|
if (!q || p.full_name.toLowerCase().includes(q)) {
|
|
out.push({
|
|
icon: '📂', label: p.full_name, sub: 'Projet · ouvrir le board',
|
|
href: '/board/' + p.full_name,
|
|
});
|
|
}
|
|
});
|
|
issues.forEach(function (i) {
|
|
out.push({
|
|
icon: '🐛',
|
|
label: '#' + i.number + ' · ' + i.title,
|
|
sub: i.repo + ' · ' + i.column_label + (i.state === 'closed' ? ' · fermée' : ''),
|
|
href: '/board/' + i.repo,
|
|
});
|
|
});
|
|
return out.slice(0, 14);
|
|
}
|
|
|
|
function render(query) {
|
|
items = buildItems(query);
|
|
activeIndex = Math.min(activeIndex, Math.max(0, items.length - 1));
|
|
list.innerHTML = '';
|
|
if (!items.length) {
|
|
var empty = document.createElement('li');
|
|
empty.className = 'palette-empty';
|
|
empty.textContent = 'Aucun résultat — essaie un nom de projet ou un mot du titre d\'une issue.';
|
|
list.appendChild(empty);
|
|
return;
|
|
}
|
|
items.forEach(function (item, idx) {
|
|
var li = document.createElement('li');
|
|
li.className = 'palette-item' + (idx === activeIndex ? ' active' : '');
|
|
var icon = document.createElement('span');
|
|
icon.className = 'palette-item-icon';
|
|
icon.textContent = item.icon;
|
|
var main = document.createElement('span');
|
|
main.className = 'palette-item-main';
|
|
main.textContent = item.label;
|
|
var sub = document.createElement('span');
|
|
sub.className = 'palette-item-sub';
|
|
sub.textContent = item.sub;
|
|
li.appendChild(icon);
|
|
li.appendChild(main);
|
|
li.appendChild(sub);
|
|
li.addEventListener('click', function () { go(item); });
|
|
li.addEventListener('mousemove', function () {
|
|
if (activeIndex !== idx) { activeIndex = idx; render(input.value.trim()); }
|
|
});
|
|
list.appendChild(li);
|
|
});
|
|
}
|
|
|
|
function go(item) {
|
|
if (item) window.location.href = item.href;
|
|
}
|
|
|
|
function searchIssues(query) {
|
|
if (query.length < 2) { issues = []; render(query); return; }
|
|
fetch('/api/palette?q=' + encodeURIComponent(query))
|
|
.then(function (r) { return r.json(); })
|
|
.then(function (data) {
|
|
if (input.value.trim() !== query) return; // stale response
|
|
issues = data.issues || [];
|
|
if (projects === null) projects = data.projects || [];
|
|
render(query);
|
|
})
|
|
.catch(function () { /* keep previous results */ });
|
|
}
|
|
|
|
input.addEventListener('input', function () {
|
|
var query = input.value.trim();
|
|
activeIndex = 0;
|
|
render(query);
|
|
if (query !== lastQuery) {
|
|
lastQuery = query;
|
|
clearTimeout(debounce);
|
|
debounce = setTimeout(function () { searchIssues(query); }, 180);
|
|
}
|
|
});
|
|
|
|
input.addEventListener('keydown', function (e) {
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
activeIndex = Math.min(activeIndex + 1, items.length - 1);
|
|
render(input.value.trim());
|
|
} else if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
activeIndex = Math.max(activeIndex - 1, 0);
|
|
render(input.value.trim());
|
|
} else if (e.key === 'Enter') {
|
|
e.preventDefault();
|
|
go(items[activeIndex]);
|
|
}
|
|
});
|
|
|
|
overlay.addEventListener('click', function (e) {
|
|
if (e.target === overlay) close();
|
|
});
|
|
|
|
document.addEventListener('keydown', function (e) {
|
|
var combo = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'k';
|
|
if (combo) {
|
|
e.preventDefault();
|
|
if (overlay.hidden) open(); else close();
|
|
} else if (e.key === 'Escape' && !overlay.hidden) {
|
|
close();
|
|
}
|
|
});
|
|
|
|
document.querySelectorAll('.palette-trigger').forEach(function (btn) {
|
|
btn.addEventListener('click', function () { open(); });
|
|
});
|
|
})();
|