feat: bounty board, task lifecycle, AI work performer flow, notifications (phase 8)
- whitelist HTML sanitizer (stdlib tokenizer) with XSS vector tests - developer board: pool-scoped visibility, customer/search/minBounty/sort filters, stale-task age badges, competing-claims visibility setting, saved filters in profile extras - claims: request/withdraw (developer), approve/decline (consultant) with notifications to winners and losers; unassign/abandon back to board - work tracking: start, sanitized comments with @mention notifications, time logging, submit for review - review queue + review with per-AC checklist stored on the timeline; approve writes the immutable bountyAwards row (human assignees only) - assign-to-AI: §5.2 job submission, HMAC-verified callback endpoint, idempotent by jobId, artifacts downloaded into GridFS, failure path keeps the task assigned with timeline + notification - notifications API + bell with unread badge, dropdown, page, WS toasts - pages: bounty board, my-tasks kanban, task detail (role-driven actions, review dialog, AI dialog), review queue, developer pool Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import { api } from '/static/js/api.js';
|
||||
import { subscribe, onPollFallback } from '/static/js/ws.js';
|
||||
|
||||
const host = document.getElementById('review-list');
|
||||
const errorBox = document.getElementById('error');
|
||||
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
||||
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
||||
}[c]));
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await api('GET', '/api/v1/consultant/reviews');
|
||||
host.replaceChildren(...res.tasks.map((t) => {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'card';
|
||||
const who = t.assignee
|
||||
? (t.assignee.kind === 'ai' ? 'AI work performer' : 'developer')
|
||||
: 'unknown';
|
||||
card.innerHTML = `
|
||||
<div class="spread">
|
||||
<h3><a href="/tasks/${t.id}">${esc(t.title)}</a></h3>
|
||||
<span class="badge accent">◈ ${t.bounty}</span>
|
||||
</div>
|
||||
<p class="muted">submitted by ${who} · updated ${new Date(t.updatedAt).toLocaleString()}</p>
|
||||
<a class="btn primary" href="/tasks/${t.id}">Open review</a>`;
|
||||
return card;
|
||||
}));
|
||||
if (!res.tasks.length) {
|
||||
host.innerHTML = '<p class="muted">Nothing waiting for review. 🎉</p>';
|
||||
}
|
||||
} catch (e) { errorBox.textContent = e.message; }
|
||||
}
|
||||
|
||||
subscribe('board', () => setTimeout(load, 300));
|
||||
onPollFallback(load);
|
||||
load();
|
||||
Reference in New Issue
Block a user