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:
+29
-6
@@ -192,14 +192,37 @@ func (s *Server) routesWeb(mux *http.ServeMux) {
|
||||
})
|
||||
}))
|
||||
|
||||
rolePage := func(path, tmpl, title, active, script string, check func(*domain.User) bool) {
|
||||
mux.HandleFunc("GET "+path, s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
|
||||
if check != nil && !check(u) && !u.Roles.Admin {
|
||||
s.render(w, r, "placeholder.html", &pageData{Title: "Not authorized", User: u})
|
||||
return
|
||||
}
|
||||
s.render(w, r, tmpl, &pageData{
|
||||
Title: title, Active: active, User: u, Scripts: []string{script},
|
||||
})
|
||||
}))
|
||||
}
|
||||
isDev := func(u *domain.User) bool { return u.Roles.Developer }
|
||||
isCons := func(u *domain.User) bool { return u.Roles.Consultant }
|
||||
rolePage("/board", "board.html", "Bounty Board", "board", "/static/js/board.js", isDev)
|
||||
rolePage("/my-tasks", "my-tasks.html", "My Tasks", "my-tasks", "/static/js/my-tasks.js", isDev)
|
||||
rolePage("/consultant/reviews", "reviews.html", "Review Queue", "reviews", "/static/js/reviews.js", isCons)
|
||||
rolePage("/consultant/pool", "pool.html", "Developer Pool", "pool", "/static/js/pool.js", isCons)
|
||||
rolePage("/notifications", "notifications.html", "Notifications", "", "/static/js/notifications-page.js", nil)
|
||||
|
||||
mux.HandleFunc("GET /tasks/{id}", s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
|
||||
s.render(w, r, "task.html", &pageData{
|
||||
Title: "Task", User: u,
|
||||
Scripts: []string{"/static/js/task.js"},
|
||||
Data: map[string]any{"TaskID": r.PathValue("id")},
|
||||
})
|
||||
}))
|
||||
|
||||
// Placeholders for areas built in later phases; replaced as they land.
|
||||
placeholders := map[string]string{
|
||||
"/board": "Bounty Board",
|
||||
"/my-tasks": "My Tasks",
|
||||
"/consultant/reviews": "Review Queue",
|
||||
"/consultant/pool": "Developer Pool",
|
||||
"/messages": "Messages",
|
||||
"/metrics": "Metrics",
|
||||
"/messages": "Messages",
|
||||
"/metrics": "Metrics",
|
||||
}
|
||||
for path, title := range placeholders {
|
||||
mux.HandleFunc("GET "+path, s.page(func(w http.ResponseWriter, r *http.Request, u *domain.User) {
|
||||
|
||||
Reference in New Issue
Block a user