diff --git a/internal/http/board.go b/internal/http/board.go index 50a4c57..7294102 100644 --- a/internal/http/board.go +++ b/internal/http/board.go @@ -19,7 +19,9 @@ import ( func (s *Server) routesBoard(mux *http.ServeMux) { dev := func(h http.HandlerFunc) http.Handler { return s.authedRole("developer", h) } - mux.Handle("GET /api/v1/board", s.requireAuth(s.requireRole("developer", http.HandlerFunc(s.handleBoard)))) + // Developers and consultants can both read the board (consultants browse to + // open task detail; claim/start/etc. below stay developer-only). + mux.Handle("GET /api/v1/board", s.requireAuth(http.HandlerFunc(s.handleBoard))) mux.Handle("GET /api/v1/my-tasks", s.requireAuth(s.requireRole("developer", http.HandlerFunc(s.handleMyTasks)))) mux.Handle("POST /api/v1/tasks/{id}/claim", dev(s.handleClaim)) mux.Handle("POST /api/v1/tasks/{id}/claim/withdraw", dev(s.handleWithdrawClaim)) @@ -39,17 +41,10 @@ func (s *Server) routesBoard(mux *http.ServeMux) { // assigned to a customer manage its tasks, so a task published by any of // them belongs to the same board. func (s *Server) boardCustomers(r *http.Request) ([]domain.Customer, error) { - consultants, err := s.store.PoolConsultantIDs(r.Context(), CurrentUser(r.Context()).ID) - if err != nil { - return nil, err - } + me := CurrentUser(r.Context()) seen := map[string]bool{} out := []domain.Customer{} - for _, cid := range consultants { - customers, err := s.store.ListCustomers(r.Context(), cid, false) - if err != nil { - return nil, err - } + add := func(customers []domain.Customer) { for _, c := range customers { if !seen[c.ID] { seen[c.ID] = true @@ -57,6 +52,28 @@ func (s *Server) boardCustomers(r *http.Request) ([]domain.Customer, error) { } } } + // Developers: every customer that has a consultant who pooled them. + if me.Roles.Developer { + consultants, err := s.store.PoolConsultantIDs(r.Context(), me.ID) + if err != nil { + return nil, err + } + for _, cid := range consultants { + customers, err := s.store.ListCustomers(r.Context(), cid, false) + if err != nil { + return nil, err + } + add(customers) + } + } + // Consultants: every customer they are assigned to. + if me.Roles.Consultant { + customers, err := s.store.ListCustomers(r.Context(), me.ID, false) + if err != nil { + return nil, err + } + add(customers) + } return out, nil } diff --git a/internal/http/web.go b/internal/http/web.go index aa4b0b7..783ccba 100644 --- a/internal/http/web.go +++ b/internal/http/web.go @@ -238,7 +238,10 @@ func (s *Server) routesWeb(mux *http.ServeMux) { } 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) + isDevOrCons := func(u *domain.User) bool { return u.Roles.Developer || u.Roles.Consultant } + // Consultants can browse the board too (read-only: they open task detail to + // review comments/assignments); only developers see claim actions. + rolePage("/board", "board.html", "Bounty Board", "board", "/static/js/board.js", isDevOrCons) 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) diff --git a/web/static/css/app.css b/web/static/css/app.css index b6fccd2..4d93eea 100644 --- a/web/static/css/app.css +++ b/web/static/css/app.css @@ -766,6 +766,23 @@ input[type=range] { width: 100%; accent-color: var(--accent); } border-color: color-mix(in srgb, var(--accent) 70%, black); } +/* ---- @-mention autocomplete menu ---- */ +.mention-menu { + position: absolute; z-index: 80; + background: var(--surface); border: var(--hairline); + border-radius: var(--radius); + box-shadow: var(--ink-shadow); + max-height: 220px; overflow-y: auto; + padding: 4px; +} +.mention-row { + display: block; width: 100%; text-align: left; + font: inherit; font-size: 0.9rem; + background: none; border: none; color: var(--text); + padding: 7px 10px; cursor: pointer; border-radius: var(--radius); +} +.mention-row.active, .mention-row:hover { background: var(--surface2); } + /* ---- dialogs: paper slips ---- */ dialog { background: var(--surface); color: var(--text); diff --git a/web/static/js/atomization.js b/web/static/js/atomization.js index 9c5bc3b..dc3f4f6 100644 --- a/web/static/js/atomization.js +++ b/web/static/js/atomization.js @@ -82,7 +82,7 @@ function renderRoot(root) { ? 'orphaned' : ''; card.innerHTML = `
${esc((root.description || '').slice(0, 240))}
@@ -135,7 +135,7 @@ function renderChildren(parent, kids, depth) {