From 07e31ce2159a088592c6cd9d8645206ab8478178 Mon Sep 17 00:00:00 2001 From: Brandon Zhang Date: Fri, 27 Mar 2026 18:32:18 +0800 Subject: [PATCH] Fix consumed instruction ordering and clear history --- go-server/internal/api/instructions.go | 11 +++++++++-- go-server/internal/store/instruction.go | 5 +++-- static/js/instructions.js | 25 ++++++++++++++++++------- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/go-server/internal/api/instructions.go b/go-server/internal/api/instructions.go index 2d31052..f8c519e 100644 --- a/go-server/internal/api/instructions.go +++ b/go-server/internal/api/instructions.go @@ -106,14 +106,21 @@ func handleDeleteInstruction(stores Stores, broker *events.Broker) http.HandlerF func handleClearConsumed(stores Stores, broker *events.Broker) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { + items, err := stores.Instructions.List("consumed") + if err != nil { + writeError(w, http.StatusInternalServerError, err.Error()) + return + } + if err := stores.Instructions.DeleteConsumed(); err != nil { writeError(w, http.StatusInternalServerError, err.Error()) return } counts, _ := stores.Instructions.Counts() - broker.Broadcast("history.cleared", nil) + cleared := len(items) + broker.Broadcast("history.cleared", map[string]any{"count": cleared}) broker.Broadcast("status.changed", map[string]any{"queue": counts}) - w.WriteHeader(http.StatusNoContent) + writeJSON(w, http.StatusOK, map[string]any{"cleared": cleared}) } } diff --git a/go-server/internal/store/instruction.go b/go-server/internal/store/instruction.go index 32877f3..46bcd19 100644 --- a/go-server/internal/store/instruction.go +++ b/go-server/internal/store/instruction.go @@ -135,9 +135,10 @@ func (s *InstructionStore) Create(content string) (*models.Instruction, error) { id := uuid.New().String() now := time.Now().UTC() - // Assign next position + // Assign next position from the full instruction history so positions remain + // globally stable even after the pending queue empties. var maxPos sql.NullInt64 - _ = s.db.QueryRow(`SELECT MAX(position) FROM instructions WHERE status = 'pending'`).Scan(&maxPos) + _ = s.db.QueryRow(`SELECT MAX(position) FROM instructions`).Scan(&maxPos) position := int(maxPos.Int64) + 1 _, err := s.db.Exec(` diff --git a/static/js/instructions.js b/static/js/instructions.js index 420c191..abba565 100644 --- a/static/js/instructions.js +++ b/static/js/instructions.js @@ -45,6 +45,16 @@ function fmtAbsTime(isoStr) { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } +function comparePending(a, b) { + return (a.position - b.position) || String(a.created_at).localeCompare(String(b.created_at)); +} + +function compareConsumed(a, b) { + const consumedDelta = new Date(b.consumed_at || 0).getTime() - new Date(a.consumed_at || 0).getTime(); + if (consumedDelta !== 0) return consumedDelta; + return b.position - a.position; +} + /** Refresh all relative-time spans in the lists (called by app.js on a timer). */ export function refreshTimestamps() { document.querySelectorAll('[data-ts]').forEach(el => { @@ -62,7 +72,7 @@ function renderPendingCard(item, index) { card.innerHTML = `
- #${item.position} + #${index + 1} ${fmtRelativeTime(item.created_at)}
${escapeHtml(item.content)}
@@ -141,14 +151,14 @@ function renderPendingCard(item, index) { return card; } -function renderConsumedCard(item) { +function renderConsumedCard(item, index) { const card = document.createElement('div'); card.className = 'instruction-card instruction-card--consumed'; card.dataset.id = item.id; card.innerHTML = `
- #${item.position} + #${index + 1} ${fmtAbsTime(item.consumed_at)} ${item.consumed_by_agent_id ? `→ ${escapeHtml(item.consumed_by_agent_id)}` @@ -171,8 +181,8 @@ export function initInstructions() { function render(instructions) { if (!instructions) return; - const pending = instructions.filter(i => i.status === 'pending'); - const consumed = instructions.filter(i => i.status === 'consumed').reverse(); + const pending = instructions.filter(i => i.status === 'pending').sort(comparePending); + const consumed = instructions.filter(i => i.status === 'consumed').sort(compareConsumed); // Pending pendingList.innerHTML = ''; @@ -194,7 +204,7 @@ export function initInstructions() { consumedList.innerHTML = `
No consumed instructions yet
`; clearHistoryBtn.style.display = 'none'; } else { - consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item))); + consumed.forEach((item, i) => consumedList.appendChild(renderConsumedCard(item, i))); clearHistoryBtn.style.display = ''; } consumedBadge.textContent = consumed.length; @@ -209,7 +219,8 @@ export function initInstructions() { clearHistoryBtn.disabled = true; try { const res = await api.clearConsumed(); - toast(`Cleared ${res.cleared} consumed instruction${res.cleared !== 1 ? 's' : ''}`, 'info'); + const cleared = res?.cleared ?? 0; + toast(`Cleared ${cleared} consumed instruction${cleared !== 1 ? 's' : ''}`, 'info'); } catch (e) { toast(e.message, 'error'); } finally {