Fix consumed instruction ordering and clear history
This commit is contained in:
@@ -106,14 +106,21 @@ func handleDeleteInstruction(stores Stores, broker *events.Broker) http.HandlerF
|
|||||||
|
|
||||||
func handleClearConsumed(stores Stores, broker *events.Broker) http.HandlerFunc {
|
func handleClearConsumed(stores Stores, broker *events.Broker) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
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 {
|
if err := stores.Instructions.DeleteConsumed(); err != nil {
|
||||||
writeError(w, http.StatusInternalServerError, err.Error())
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
counts, _ := stores.Instructions.Counts()
|
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})
|
broker.Broadcast("status.changed", map[string]any{"queue": counts})
|
||||||
w.WriteHeader(http.StatusNoContent)
|
writeJSON(w, http.StatusOK, map[string]any{"cleared": cleared})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,10 @@ func (s *InstructionStore) Create(content string) (*models.Instruction, error) {
|
|||||||
id := uuid.New().String()
|
id := uuid.New().String()
|
||||||
now := time.Now().UTC()
|
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
|
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
|
position := int(maxPos.Int64) + 1
|
||||||
|
|
||||||
_, err := s.db.Exec(`
|
_, err := s.db.Exec(`
|
||||||
|
|||||||
@@ -45,6 +45,16 @@ function fmtAbsTime(isoStr) {
|
|||||||
return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
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). */
|
/** Refresh all relative-time spans in the lists (called by app.js on a timer). */
|
||||||
export function refreshTimestamps() {
|
export function refreshTimestamps() {
|
||||||
document.querySelectorAll('[data-ts]').forEach(el => {
|
document.querySelectorAll('[data-ts]').forEach(el => {
|
||||||
@@ -62,7 +72,7 @@ function renderPendingCard(item, index) {
|
|||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="instruction-card__meta">
|
<div class="instruction-card__meta">
|
||||||
<span class="instruction-card__pos">#${item.position}</span>
|
<span class="instruction-card__pos">#${index + 1}</span>
|
||||||
<span class="instruction-card__time" data-ts="${item.created_at}">${fmtRelativeTime(item.created_at)}</span>
|
<span class="instruction-card__time" data-ts="${item.created_at}">${fmtRelativeTime(item.created_at)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="instruction-card__content">${escapeHtml(item.content)}</div>
|
<div class="instruction-card__content">${escapeHtml(item.content)}</div>
|
||||||
@@ -141,14 +151,14 @@ function renderPendingCard(item, index) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderConsumedCard(item) {
|
function renderConsumedCard(item, index) {
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'instruction-card instruction-card--consumed';
|
card.className = 'instruction-card instruction-card--consumed';
|
||||||
card.dataset.id = item.id;
|
card.dataset.id = item.id;
|
||||||
|
|
||||||
card.innerHTML = `
|
card.innerHTML = `
|
||||||
<div class="instruction-card__meta">
|
<div class="instruction-card__meta">
|
||||||
<span class="instruction-card__pos">#${item.position}</span>
|
<span class="instruction-card__pos">#${index + 1}</span>
|
||||||
<span class="instruction-card__time">${fmtAbsTime(item.consumed_at)}</span>
|
<span class="instruction-card__time">${fmtAbsTime(item.consumed_at)}</span>
|
||||||
${item.consumed_by_agent_id
|
${item.consumed_by_agent_id
|
||||||
? `<span class="instruction-card__consumed-by">→ ${escapeHtml(item.consumed_by_agent_id)}</span>`
|
? `<span class="instruction-card__consumed-by">→ ${escapeHtml(item.consumed_by_agent_id)}</span>`
|
||||||
@@ -171,8 +181,8 @@ export function initInstructions() {
|
|||||||
|
|
||||||
function render(instructions) {
|
function render(instructions) {
|
||||||
if (!instructions) return;
|
if (!instructions) return;
|
||||||
const pending = instructions.filter(i => i.status === 'pending');
|
const pending = instructions.filter(i => i.status === 'pending').sort(comparePending);
|
||||||
const consumed = instructions.filter(i => i.status === 'consumed').reverse();
|
const consumed = instructions.filter(i => i.status === 'consumed').sort(compareConsumed);
|
||||||
|
|
||||||
// Pending
|
// Pending
|
||||||
pendingList.innerHTML = '';
|
pendingList.innerHTML = '';
|
||||||
@@ -194,7 +204,7 @@ export function initInstructions() {
|
|||||||
consumedList.innerHTML = `<div class="empty-state"><div class="empty-state__icon">◈</div>No consumed instructions yet</div>`;
|
consumedList.innerHTML = `<div class="empty-state"><div class="empty-state__icon">◈</div>No consumed instructions yet</div>`;
|
||||||
clearHistoryBtn.style.display = 'none';
|
clearHistoryBtn.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
consumed.forEach(item => consumedList.appendChild(renderConsumedCard(item)));
|
consumed.forEach((item, i) => consumedList.appendChild(renderConsumedCard(item, i)));
|
||||||
clearHistoryBtn.style.display = '';
|
clearHistoryBtn.style.display = '';
|
||||||
}
|
}
|
||||||
consumedBadge.textContent = consumed.length;
|
consumedBadge.textContent = consumed.length;
|
||||||
@@ -209,7 +219,8 @@ export function initInstructions() {
|
|||||||
clearHistoryBtn.disabled = true;
|
clearHistoryBtn.disabled = true;
|
||||||
try {
|
try {
|
||||||
const res = await api.clearConsumed();
|
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) {
|
} catch (e) {
|
||||||
toast(e.message, 'error');
|
toast(e.message, 'error');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user