Nests: team rooms with core crows and borrowing
- Server: nests API, persistence, core-first picking, borrowed flag (built by the backend agent against EVENTS.md; tests pass with -race) - UI: nests in the sidebar, one feed per nest, header shows the core team, New nest dialog, borrowed crows labeled, per-nest waiting count - start.command: one-click local server in controlled mode (mock crows, localhost only, state in .crows/) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQnSw4RRWp8uqpj3nsnnXF
This commit is contained in:
14 files changed
+782
-62
No files matched your search
@@ -31,12 +31,13 @@ type Server struct {
|
||||
p *Pipeline
|
||||
approver *WebApprover
|
||||
hub *Hub
|
||||
nests *NestStore
|
||||
ctx context.Context // cancelled on shutdown; lists and streams end with it
|
||||
beat time.Duration
|
||||
}
|
||||
|
||||
func NewServer(ctx context.Context, p *Pipeline, approver *WebApprover) *Server {
|
||||
return &Server{p: p, approver: approver, hub: NewHub(), ctx: ctx, beat: heartbeat}
|
||||
func NewServer(ctx context.Context, p *Pipeline, approver *WebApprover, nests *NestStore) *Server {
|
||||
return &Server{p: p, approver: approver, hub: NewHub(), nests: nests, ctx: ctx, beat: heartbeat}
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
@@ -45,6 +46,8 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("GET /api/events", s.events)
|
||||
mux.HandleFunc("POST /api/verdicts", s.postVerdict)
|
||||
mux.HandleFunc("GET /api/trust", s.trust)
|
||||
mux.HandleFunc("GET /api/nests", s.getNests)
|
||||
mux.HandleFunc("POST /api/nests", s.postNest)
|
||||
if st, err := os.Stat(webDir); err == nil && st.IsDir() {
|
||||
mux.Handle("GET /", http.FileServer(http.Dir(webDir)))
|
||||
} else {
|
||||
@@ -67,13 +70,15 @@ func cmdServe(args []string) {
|
||||
flog, err := OpenFlightLog(filepath.Join(dataDir, "flight.jsonl"))
|
||||
must(err)
|
||||
defer flog.Close()
|
||||
nests, err := LoadNests(filepath.Join(dataDir, "nests.json"))
|
||||
must(err)
|
||||
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
brain := NewBrain()
|
||||
approver := NewWebApprover(*timeout)
|
||||
p := &Pipeline{Flock: NewFlock(brain, g, flog), Ledger: ledger, Log: flog, G: g, Approver: approver}
|
||||
s := NewServer(ctx, p, approver)
|
||||
s := NewServer(ctx, p, approver, nests)
|
||||
|
||||
srv := &http.Server{Addr: *addr, Handler: s.Handler(), ReadHeaderTimeout: 10 * time.Second}
|
||||
go func() {
|
||||
@@ -120,10 +125,19 @@ func (s *Server) postList(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Text string `json:"text"`
|
||||
Requester string `json:"requester"`
|
||||
NestID string `json:"nest_id"`
|
||||
}
|
||||
if !decode(w, r, &body) {
|
||||
return
|
||||
}
|
||||
if body.NestID == "" {
|
||||
body.NestID = defaultNestID
|
||||
}
|
||||
nest, found := s.nests.Get(body.NestID)
|
||||
if !found {
|
||||
httpError(w, http.StatusNotFound, fmt.Sprintf("There is no nest called %q.", body.NestID))
|
||||
return
|
||||
}
|
||||
if body.Requester == "" {
|
||||
body.Requester, _, _ = net.SplitHostPort(r.RemoteAddr)
|
||||
}
|
||||
@@ -157,7 +171,7 @@ func (s *Server) postList(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
id := newListID()
|
||||
s.hub.Open(id, body.Requester)
|
||||
l := List{ID: id, Requester: body.Requester, Source: "web", Items: items, Parallel: true}
|
||||
l := List{ID: id, Requester: body.Requester, Source: "web", Items: items, Nest: nest, Parallel: true}
|
||||
go s.p.Run(s.ctx, l, s.hub.Emitter(id))
|
||||
writeJSON(w, http.StatusAccepted, map[string]string{"list_id": id})
|
||||
}
|
||||
@@ -253,3 +267,29 @@ func (s *Server) trust(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) getNests(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]any{"roster": roster, "nests": s.nests.All()})
|
||||
}
|
||||
|
||||
func (s *Server) postNest(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Name string `json:"name"`
|
||||
Crows []string `json:"crows"`
|
||||
}
|
||||
if !decode(w, r, &body) {
|
||||
return
|
||||
}
|
||||
n, err := s.nests.Create(body.Name, body.Crows)
|
||||
var bad *NestError
|
||||
switch {
|
||||
case errors.As(err, &bad) && bad.Clash:
|
||||
httpError(w, http.StatusConflict, bad.Msg)
|
||||
case errors.As(err, &bad):
|
||||
httpError(w, http.StatusBadRequest, bad.Msg)
|
||||
case err != nil:
|
||||
httpError(w, http.StatusInternalServerError, "could not save the nest: "+err.Error())
|
||||
default:
|
||||
writeJSON(w, http.StatusCreated, n)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user