From 604d4376b43946c1b938df917257171f29098dab Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 10 Apr 2026 09:48:20 +0200 Subject: [PATCH] feat: two-step issue resolution (resolve then delete) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- main.py | 20 +++++++++++++++- static/script.js | 60 +++++++++++++++++++++++++++++++++++++++--------- static/style.css | 56 ++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 120 insertions(+), 16 deletions(-) diff --git a/main.py b/main.py index df85b4fd..0846156d 100644 --- a/main.py +++ b/main.py @@ -1368,6 +1368,7 @@ async def add_issue(issue: dict): "category": issue.get("category", "other"), "author": issue.get("author", "Anonymous").strip(), "created": datetime.utcnow().isoformat(), + "resolved": False, } if not new_issue["title"]: raise HTTPException(status_code=400, detail="Title is required") @@ -1376,9 +1377,26 @@ async def add_issue(issue: dict): return new_issue +@app.patch("/issues/{issue_id}") +async def update_issue(issue_id: str, update: dict): + """Update an issue (e.g. toggle resolved state).""" + issues = _load_issues() + found = None + for i in issues: + if i.get("id") == issue_id: + if "resolved" in update: + i["resolved"] = bool(update["resolved"]) + found = i + break + if not found: + raise HTTPException(status_code=404, detail="Issue not found") + _save_issues(issues) + return found + + @app.delete("/issues/{issue_id}") async def delete_issue(issue_id: str): - """Resolve (delete) an issue.""" + """Permanently delete an issue.""" issues = _load_issues() issues = [i for i in issues if i.get("id") != issue_id] _save_issues(issues) diff --git a/static/script.js b/static/script.js index 952978ef..4b6f8882 100644 --- a/static/script.js +++ b/static/script.js @@ -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 = `
${cat.label} - ${issue.title} + ${issue.title} ${date}
- ${issue.description ? `
${issue.description}
` : ''} + ${issue.description ? `
${issue.description}
` : ''} `; - 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); }); diff --git a/static/style.css b/static/style.css index 22812b01..61254ed8 100644 --- a/static/style.css +++ b/static/style.css @@ -2664,12 +2664,35 @@ table.ts-allegiance td:first-child { .issue-row { padding: 6px 8px; border-bottom: 1px solid #333; + border-left: 3px solid transparent; + color: #ddd; } .issue-row:hover { background: #1a1a2a; } +.issue-row .issue-description { + color: #aaa; + font-size: 0.75rem; + margin-top: 2px; + padding-left: 4px; +} + +.issue-row.issue-resolved { + background: rgba(74, 170, 74, 0.12); + border-left-color: #4a4; +} + +.issue-row.issue-resolved strong { + text-decoration: line-through; + color: #888; +} + +.issue-row.issue-resolved .issue-description { + color: #666; +} + .issue-category { display: inline-block; padding: 1px 6px; @@ -2680,15 +2703,20 @@ table.ts-allegiance td:first-child { text-transform: uppercase; } -.issue-resolve-btn { +.issue-resolve-btn, +.issue-reopen-btn, +.issue-delete-btn { padding: 1px 6px; font-size: 0.65rem; background: transparent; - color: #4a4; - border: 1px solid #4a4; cursor: pointer; border-radius: 3px; - margin-left: 8px; + margin-left: 4px; +} + +.issue-resolve-btn { + color: #4a4; + border: 1px solid #4a4; } .issue-resolve-btn:hover { @@ -2696,6 +2724,26 @@ table.ts-allegiance td:first-child { color: #fff; } +.issue-reopen-btn { + color: #88a; + border: 1px solid #88a; +} + +.issue-reopen-btn:hover { + background: #88a; + color: #fff; +} + +.issue-delete-btn { + color: #c44; + border: 1px solid #c44; +} + +.issue-delete-btn:hover { + background: #c44; + color: #fff; +} + .issues-form { padding: 6px 8px; border-top: 1px solid #444;