70a813edfa
- 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>
42 lines
1.6 KiB
JavaScript
42 lines
1.6 KiB
JavaScript
import { api, toast } from '/static/js/api.js';
|
|
|
|
const host = document.getElementById('notification-page-list');
|
|
const errorBox = document.getElementById('error');
|
|
let cursor = '';
|
|
|
|
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({
|
|
'&': '&', '<': '<', '>': '>', '"': '"', "'": ''',
|
|
}[c]));
|
|
|
|
async function load(reset) {
|
|
try {
|
|
if (reset) { cursor = ''; host.replaceChildren(); }
|
|
const res = await api('GET', `/api/v1/notifications?limit=30&cursor=${cursor}`);
|
|
const items = res.notifications;
|
|
cursor = items.length ? items[items.length - 1].id : cursor;
|
|
host.append(...items.map((n) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'card';
|
|
div.style.padding = '12px';
|
|
div.innerHTML = `
|
|
<div class="spread">
|
|
<a href="${esc(n.link || '#')}"><strong>${esc(n.title)}</strong></a>
|
|
<span class="muted">${new Date(n.createdAt).toLocaleString()} ${n.readAt ? '' : '· <span class="badge accent">new</span>'}</span>
|
|
</div>
|
|
${n.body ? `<p class="muted" style="margin:4px 0 0">${esc(n.body)}</p>` : ''}`;
|
|
return div;
|
|
}));
|
|
document.getElementById('notif-more').hidden = items.length < 30;
|
|
} catch (e) { errorBox.textContent = e.message; }
|
|
}
|
|
|
|
document.getElementById('mark-all').addEventListener('click', async () => {
|
|
try {
|
|
await api('POST', '/api/v1/notifications/read', { ids: [] });
|
|
toast('All notifications marked read.', 'ok');
|
|
load(true);
|
|
} catch (e) { toast(e.message, 'err'); }
|
|
});
|
|
document.getElementById('notif-more').addEventListener('click', () => load(false));
|
|
load(true);
|