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:
etalon
2026-06-12 20:11:05 +02:00
parent f87b954f27
commit 70a813edfa
28 changed files with 2764 additions and 14 deletions
+112
View File
@@ -0,0 +1,112 @@
// Package workperform is the app-side client of the Work Performer Service
// (§5.2) — fully independent from the atomizer: separate base URL, token,
// and circuit breaker.
package workperform
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"net/http"
"time"
"bountyboard/internal/extsvc"
)
type Attachment struct {
Name string `json:"name"`
URL string `json:"url"`
MimeType string `json:"mimeType"`
}
type JobContext struct {
RepositoryURL string `json:"repositoryUrl,omitempty"`
Branch string `json:"branch,omitempty"`
Instructions string `json:"instructions,omitempty"`
}
type JobRequest struct {
TaskID string `json:"taskId"`
Title string `json:"title"`
Description string `json:"description"`
AcceptanceCriteria []string `json:"acceptanceCriteria"`
Attachments []Attachment `json:"attachments"`
Links []string `json:"links"`
Context JobContext `json:"context"`
CallbackURL string `json:"callbackUrl"`
}
type JobAccepted struct {
JobID string `json:"jobId"`
Status string `json:"status"`
}
type JobStatus struct {
JobID string `json:"jobId"`
Status string `json:"status"` // queued | running | succeeded | failed
StartedAt string `json:"startedAt"`
FinishedAt string `json:"finishedAt"`
}
// Callback is the §5.2 service→app result payload.
type Callback struct {
JobID string `json:"jobId"`
TaskID string `json:"taskId"`
Status string `json:"status"` // succeeded | failed
Summary string `json:"summary"`
Artifacts []struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"artifacts"`
Log string `json:"log"`
}
type Client struct {
c *extsvc.Client
token string
}
func New(baseURL func() string, token string, timeout time.Duration) *Client {
return &Client{c: extsvc.NewClient("work-performer", baseURL, token, timeout), token: token}
}
func (w *Client) BreakerState() string { return w.c.BreakerState() }
func (w *Client) Healthy(ctx context.Context) error { return w.c.Healthy(ctx) }
func (w *Client) Submit(ctx context.Context, req JobRequest) (*JobAccepted, error) {
var out JobAccepted
if err := w.c.Call(ctx, http.MethodPost, "/v1/jobs", req, &out); err != nil {
return nil, err
}
return &out, nil
}
func (w *Client) Status(ctx context.Context, jobID string) (*JobStatus, error) {
var out JobStatus
if err := w.c.Call(ctx, http.MethodGet, "/v1/jobs/"+jobID, nil, &out); err != nil {
return nil, err
}
return &out, nil
}
// Cancel is best effort (§5.2).
func (w *Client) Cancel(ctx context.Context, jobID string) error {
return w.c.Call(ctx, http.MethodDelete, "/v1/jobs/"+jobID, nil, nil)
}
// VerifySignature checks the §5.2 callback HMAC:
// X-Signature = hex(hmac-sha256(body, WORK_PERFORMER_TOKEN)).
func VerifySignature(token string, body []byte, signature string) bool {
mac := hmac.New(sha256.New, []byte(token))
mac.Write(body)
want := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(want), []byte(signature))
}
// Sign produces the callback signature (used by tests and the mock service).
func Sign(token string, body []byte) string {
mac := hmac.New(sha256.New, []byte(token))
mac.Write(body)
return hex.EncodeToString(mac.Sum(nil))
}