Files
BountyBoard/web/static/js/pool.js
T
etalon 70a813edfa 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>
2026-06-12 20:11:05 +02:00

57 lines
2.0 KiB
JavaScript

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();