feat(code): cartes liées au code — branches, PRs, commits (issue #1)

app/code_links.py: appariement pur (#n dans titres/corps/messages,
numéro en jeton de branche). Client: list_branches/list_pulls/
list_commits. Badge sur les cartes (board + fragments live),
section Code lié du panneau détail, /api/issues/{o}/{r}/{n}/code.
This commit is contained in:
Spark
2026-10-11 14:21:11 -04:00
parent 03fa1eb44e
commit 675ce09083
8 changed files with 545 additions and 2 deletions
+7
View File
@@ -1144,3 +1144,10 @@ footer {
.sprint-render h1 { font-size: 1.5rem; }
.sprint-render h2 { font-size: 1.15rem; margin-top: 22px; }
.sprint-render a { color: var(--accent, #58a6ff); }
/* ── Code lié (0.13.0, #1) ── */
.card-code { font-size: 0.78rem; color: var(--accent, #58a6ff); margin-top: 5px; }
.detail-code { margin: 10px 0; }
.code-line { padding: 3px 0; font-size: 0.9rem; color: var(--text-2, #b6c2d2); }
.code-line a { color: var(--accent, #58a6ff); text-decoration: none; }
.code-line code { background: var(--surface-3, #1d2942); border-radius: 4px; padding: 0 5px; }
+28
View File
@@ -369,12 +369,40 @@ document.addEventListener('DOMContentLoaded', () => {
const data = await r.json().catch(() => ({}));
if (!r.ok || !data.ok) throw new Error(data.error || ('HTTP ' + r.status));
fillDetail(data.issue, data.comments);
loadCodeLinks(repoFull, number);
} catch (err) {
document.getElementById('detail-title').textContent = 'Erreur de chargement';
showToast('❌ Détail indisponible : ' + err.message, true);
}
}
async function loadCodeLinks(repoFull, number) {
const box = document.getElementById('detail-code');
if (!box) return;
box.innerHTML = '';
const [owner, repo] = repoFull.split('/', 2);
try {
const r = await fetch(`/api/issues/${owner}/${repo}/${number}/code`);
const data = await r.json().catch(() => ({}));
if (!r.ok || !data.ok) return;
const parts = [];
if ((data.branches || []).length) {
parts.push('🌿 Branches : ' + data.branches.map(b => `<code>${b}</code>`).join(', '));
}
for (const pr of data.pulls || []) {
const state = pr.merged ? 'fusionnée' : (pr.state === 'open' ? 'ouverte' : 'fermée');
parts.push(`🔀 <a href="${pr.html_url}" target="_blank" rel="noopener">PR #${pr.number}</a> ${pr.title} — ${state}`);
}
for (const c of (data.commits || []).slice(0, 8)) {
parts.push(`⑂ <a href="${c.html_url}" target="_blank" rel="noopener"><code>${c.sha}</code></a> ${c.message}${c.date ? ' · ' + c.date : ''}`);
}
box.innerHTML = parts.length
? '<h3 class="detail-comments-title">🧬 Code lié</h3>' + parts.map(p => `<div class="code-line">${p}</div>`).join('')
: '';
} catch (err) { /* le code est un bonus, jamais bloquant */ }
}
function fillDetail(issue, comments) {
document.getElementById('detail-title').textContent = issue.title;
const stateChip = document.getElementById('detail-state');