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,46 @@
|
||||
{{define "content"}}
|
||||
<h1>Bounty Board</h1>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
|
||||
<div class="row mt" id="board-filters">
|
||||
<div class="field">
|
||||
<label for="f-q">Search</label>
|
||||
<input type="search" id="f-q" placeholder="Search title or description… ( / )">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-customer">Customer</label>
|
||||
<select id="f-customer"><option value="">All</option></select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-min">Min bounty</label>
|
||||
<input type="number" id="f-min" min="0" step="10" placeholder="0">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="f-sort">Sort</label>
|
||||
<select id="f-sort">
|
||||
<option value="">Newest</option>
|
||||
<option value="bounty">Highest bounty</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field" style="flex:0;align-self:flex-end">
|
||||
<button class="btn" id="f-save" title="Save current filters as default">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid mt" id="board-grid"></div>
|
||||
|
||||
<dialog id="claim-dialog">
|
||||
<form method="dialog" id="claim-form" class="stack" style="min-width:380px">
|
||||
<h2>Request assignment</h2>
|
||||
<p class="muted" id="claim-task-title"></p>
|
||||
<div class="field">
|
||||
<label for="claim-note">Pitch note (optional)</label>
|
||||
<textarea id="claim-note" placeholder="why you?"></textarea>
|
||||
</div>
|
||||
<div class="spread">
|
||||
<button class="btn" type="button" id="claim-cancel">Cancel</button>
|
||||
<button class="btn primary" type="submit">Request assignment</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
{{end}}
|
||||
@@ -30,6 +30,14 @@
|
||||
</div>
|
||||
<div class="right">
|
||||
<button class="btn ghost small" id="nav-theme" type="button" aria-label="Toggle dark / light theme">◐</button>
|
||||
{{if .User}}
|
||||
<span style="position:relative">
|
||||
<button class="btn ghost small" id="nav-bell" type="button" aria-label="Notifications">🔔<span class="bell-badge" id="nav-bell-count" hidden></span></button>
|
||||
<div id="notif-panel" class="notif-panel" hidden>
|
||||
<ul id="notif-list" class="notif-list"></ul>
|
||||
<a href="/notifications" class="muted">All notifications →</a>
|
||||
</div>
|
||||
</span>{{end}}
|
||||
{{if .User}}
|
||||
<a href="/profile" aria-label="Your profile">
|
||||
{{if .User.AvatarFileID}}
|
||||
@@ -49,6 +57,7 @@
|
||||
</main>
|
||||
<div id="toasts" aria-live="polite"></div>
|
||||
<script type="module" src="/static/js/nav.js"></script>
|
||||
{{if .User}}<script type="module" src="/static/js/notifications.js"></script>{{end}}
|
||||
{{range .Scripts}}<script type="module" src="{{.}}"></script>
|
||||
{{end}}
|
||||
</body>
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
{{define "content"}}
|
||||
<h1>My Tasks</h1>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<div class="kanban mt" id="kanban">
|
||||
<section class="kanban-col" data-col="assigned"><h2>Assigned</h2><div class="stack"></div></section>
|
||||
<section class="kanban-col" data-col="in_progress"><h2>In progress</h2><div class="stack"></div></section>
|
||||
<section class="kanban-col" data-col="in_review"><h2>In review</h2><div class="stack"></div></section>
|
||||
<section class="kanban-col" data-col="approved"><h2>Done</h2><div class="stack"></div></section>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,9 @@
|
||||
{{define "content"}}
|
||||
<div class="spread">
|
||||
<h1>Notifications</h1>
|
||||
<button class="btn small" id="mark-all">Mark all read</button>
|
||||
</div>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<div id="notification-page-list" class="stack mt"></div>
|
||||
<button class="btn mt" id="notif-more">Load more</button>
|
||||
{{end}}
|
||||
@@ -0,0 +1,12 @@
|
||||
{{define "content"}}
|
||||
<h1>Developer Pool</h1>
|
||||
<p class="hint">Developers in your pool see your published tasks on their bounty board.</p>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<div class="spread mt">
|
||||
<input type="search" id="pool-search" placeholder="Filter developers…" style="max-width:280px" aria-label="Filter developers">
|
||||
</div>
|
||||
<table class="list mt" id="pool-table">
|
||||
<thead><tr><th></th><th>Name</th><th>Email</th><th>Bio</th><th></th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
@@ -0,0 +1,5 @@
|
||||
{{define "content"}}
|
||||
<h1>Review Queue</h1>
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<div id="review-list" class="stack mt"></div>
|
||||
{{end}}
|
||||
@@ -0,0 +1,49 @@
|
||||
{{define "content"}}
|
||||
<div class="error-box" id="error" role="alert"></div>
|
||||
<div id="task-root" data-task-id="{{.Data.TaskID}}" data-user-id="{{.User.ID}}"
|
||||
data-is-consultant="{{if .User.Roles.Consultant}}1{{end}}"
|
||||
data-is-admin="{{if .User.Roles.Admin}}1{{end}}"
|
||||
data-is-developer="{{if .User.Roles.Developer}}1{{end}}">
|
||||
<p class="muted">Loading task…</p>
|
||||
</div>
|
||||
|
||||
<dialog id="review-dialog">
|
||||
<form method="dialog" id="review-form" class="stack" style="min-width:460px">
|
||||
<h2>Review submission</h2>
|
||||
<div id="review-checklist" class="stack"></div>
|
||||
<div class="field">
|
||||
<label for="review-note">Review note</label>
|
||||
<textarea id="review-note"></textarea>
|
||||
</div>
|
||||
<div class="spread">
|
||||
<button class="btn" type="button" id="review-cancel">Cancel</button>
|
||||
<span>
|
||||
<button class="btn danger" type="button" id="review-changes">Request changes</button>
|
||||
<button class="btn primary" type="button" id="review-approve">Approve & award bounty</button>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog id="ai-dialog">
|
||||
<form method="dialog" id="ai-form" class="stack" style="min-width:420px">
|
||||
<h2>Assign to AI work performer</h2>
|
||||
<div class="field">
|
||||
<label for="ai-repo">Repository URL (optional)</label>
|
||||
<input type="text" id="ai-repo" placeholder="https://github.com/org/repo.git">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ai-branch">Branch (optional)</label>
|
||||
<input type="text" id="ai-branch" placeholder="main">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ai-instructions">Extra instructions (optional)</label>
|
||||
<textarea id="ai-instructions"></textarea>
|
||||
</div>
|
||||
<div class="spread">
|
||||
<button class="btn" type="button" id="ai-cancel">Cancel</button>
|
||||
<button class="btn primary" type="submit">Create AI job</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user