feat: two-step issue resolution (resolve then delete)

- New PATCH /issues/{id} endpoint to toggle resolved flag
- Add resolved:false to new issues
- Frontend: click "✓ Resolve" marks issue green with strikethrough
- Resolved issues show "↺ Reopen" and "🗑 Delete" buttons
- Delete requires confirmation
- Sort: unresolved first, then resolved
- Issues persist until explicitly deleted

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-10 09:48:20 +02:00
parent f2b43fce0b
commit 604d4376b4
3 changed files with 120 additions and 16 deletions

View file

@ -4209,30 +4209,68 @@ async function refreshIssuesList(win) {
return;
}
// Sort: unresolved first, then resolved
issues.sort((a, b) => (a.resolved ? 1 : 0) - (b.resolved ? 1 : 0));
listDiv.innerHTML = '';
issues.forEach(issue => {
const cat = ISSUE_CATEGORIES[issue.category] || ISSUE_CATEGORIES.other;
const date = issue.created ? new Date(issue.created).toLocaleDateString('sv-SE') : '';
const isResolved = !!issue.resolved;
const row = document.createElement('div');
row.className = 'issue-row';
row.className = 'issue-row' + (isResolved ? ' issue-resolved' : '');
row.innerHTML = `
<div style="display:flex;align-items:center;gap:6px;">
<span class="issue-category" style="background:${cat.color}">${cat.label}</span>
<strong style="font-size:0.8rem;color:#ddd;">${issue.title}</strong>
<strong style="font-size:0.8rem;">${issue.title}</strong>
<span style="margin-left:auto;color:#666;font-size:0.65rem;">${date}</span>
</div>
${issue.description ? `<div style="color:#aaa;font-size:0.75rem;margin-top:2px;padding-left:4px;">${issue.description}</div>` : ''}
${issue.description ? `<div class="issue-description">${issue.description}</div>` : ''}
`;
const resolveBtn = document.createElement('button');
resolveBtn.textContent = '✓ Resolve';
resolveBtn.className = 'issue-resolve-btn';
resolveBtn.addEventListener('click', async () => {
await fetch(`/issues/${issue.id}`, { method: 'DELETE' });
refreshIssuesList(win);
});
row.querySelector('div').appendChild(resolveBtn);
const headerDiv = row.querySelector('div');
if (isResolved) {
// Reopen button
const reopenBtn = document.createElement('button');
reopenBtn.textContent = '↺ Reopen';
reopenBtn.className = 'issue-reopen-btn';
reopenBtn.addEventListener('click', async () => {
await fetch(`/issues/${issue.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolved: false })
});
refreshIssuesList(win);
});
headerDiv.appendChild(reopenBtn);
// Delete button
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '🗑 Delete';
deleteBtn.className = 'issue-delete-btn';
deleteBtn.addEventListener('click', async () => {
if (!confirm(`Delete issue "${issue.title}"?`)) return;
await fetch(`/issues/${issue.id}`, { method: 'DELETE' });
refreshIssuesList(win);
});
headerDiv.appendChild(deleteBtn);
} else {
// Resolve button (marks as resolved, doesn't delete)
const resolveBtn = document.createElement('button');
resolveBtn.textContent = '✓ Resolve';
resolveBtn.className = 'issue-resolve-btn';
resolveBtn.addEventListener('click', async () => {
await fetch(`/issues/${issue.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ resolved: true })
});
refreshIssuesList(win);
});
headerDiv.appendChild(resolveBtn);
}
listDiv.appendChild(row);
});