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,56 @@
|
||||
import { api, toast } from '/static/js/api.js';
|
||||
|
||||
const errorBox = document.getElementById('error');
|
||||
let developers = [];
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
const res = await api('GET', '/api/v1/consultant/pool');
|
||||
developers = res.developers;
|
||||
render();
|
||||
} catch (e) { errorBox.textContent = e.message; }
|
||||
}
|
||||
|
||||
function render() {
|
||||
const filter = document.getElementById('pool-search').value.trim().toLowerCase();
|
||||
const tbody = document.querySelector('#pool-table tbody');
|
||||
tbody.replaceChildren(...developers
|
||||
.filter((d) => !filter || d.name.toLowerCase().includes(filter) || d.email.toLowerCase().includes(filter))
|
||||
.map((d) => {
|
||||
const tr = document.createElement('tr');
|
||||
const avatar = document.createElement('td');
|
||||
if (d.avatarFileId) {
|
||||
const img = document.createElement('img');
|
||||
img.className = 'avatar';
|
||||
img.src = `/files/${d.avatarFileId}`;
|
||||
img.alt = '';
|
||||
avatar.appendChild(img);
|
||||
}
|
||||
const name = document.createElement('td');
|
||||
name.textContent = d.name;
|
||||
name.dataset.userCard = d.id;
|
||||
const email = document.createElement('td');
|
||||
email.textContent = d.email;
|
||||
const bio = document.createElement('td');
|
||||
bio.textContent = (d.bio || '').slice(0, 80);
|
||||
bio.className = 'muted';
|
||||
const action = document.createElement('td');
|
||||
const btn = document.createElement('button');
|
||||
btn.className = d.inPool ? 'btn small' : 'btn small primary';
|
||||
btn.textContent = d.inPool ? 'Remove from pool' : 'Add to pool';
|
||||
btn.addEventListener('click', async () => {
|
||||
try {
|
||||
if (d.inPool) await api('DELETE', `/api/v1/consultant/pool/${d.id}`);
|
||||
else await api('POST', '/api/v1/consultant/pool', { developerId: d.id });
|
||||
d.inPool = !d.inPool;
|
||||
render();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
});
|
||||
action.appendChild(btn);
|
||||
tr.append(avatar, name, email, bio, action);
|
||||
return tr;
|
||||
}));
|
||||
}
|
||||
|
||||
document.getElementById('pool-search').addEventListener('input', render);
|
||||
load();
|
||||
Reference in New Issue
Block a user