rony-chat-bot/internal/server/server.go

65 lines
1.5 KiB
Go
Raw Normal View History

package server
import (
"context"
"errors"
"log/slog"
"net/http"
"time"
"github.com/go-chi/chi/v5"
"github.com/VictorVargas/rony-chat-bot/internal/config"
)
type Server struct {
httpSrv *http.Server
}
func New(cfg *config.Config, h *Handlers) *Server {
r := chi.NewRouter()
r.Use(RequestID)
r.Use(Logging)
r.Use(CORS(cfg.Server.CORSOrigins))
if cfg.Server.RateLimit.RequestsPerMinute > 0 {
r.Use(RateLimit(cfg.Server.RateLimit))
}
r.Get("/", func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write([]byte("Rony Chat Bot — see /api/health, /api/info, POST /api/chat\n"))
})
r.Route("/api", func(r chi.Router) {
r.Post("/chat", h.Chat)
r.Post("/reindex", h.Reindex)
r.Get("/health", h.Health)
r.Get("/info", h.Info)
feat: persistent conversation storage (Phase 4) Conversations survive page reloads and work for any frontend, not just the widget. Server-side SQLite, conversation ID as bearer token, browser identity via localStorage. Backend ------- - internal/portfolio/conversations.go: schema + CRUD. Conversations and messages tables in the same SQLite DB as the RAG index, with foreign-key cascade delete. Conv IDs are 16-byte random hex (128 bits of entropy). - internal/portfolio/indexer.go: applies conversation schema + enables foreign_keys pragma in OpenStore. - internal/server/handlers.go: POST /api/chat accepts an optional conversation_id, mints one if absent, persists user message before the LLM runs and assistant message (with sources) after the stream completes. New handlers: GetConversation, ListConversations, DeleteConversation. - internal/server/server.go: routes for GET /api/conversations, GET/DELETE /api/conversations/{id}. - internal/server/conversations_test.go: 6 tests (round-trip, continue, list, 404, delete, streaming). Widget ------ - web/chat-widget.js: stores conv_id in localStorage["rony-chat-conv"], includes it in the chat request body, captures new IDs from the server's 'start' SSE event, and calls GET /api/conversations/{id} on load to restore history. On 404 it clears the stored ID and starts fresh. Docs ---- - docs/architecture.md: §3.1 documents the conversation_id field and new REST endpoints; new §3.4 covers persistence lifecycle, schema, client responsibilities, and auth model. §5.6 updated; filetree reflects the new files. - web/README.md: new 'Conversation persistence' section explains the browser-scoped behavior and how to opt out or persist across devices.
2026-07-17 07:56:35 +00:00
r.Get("/conversations", h.ListConversations)
r.Get("/conversations/{id}", h.GetConversation)
r.Delete("/conversations/{id}", h.DeleteConversation)
})
srv := &http.Server{
Addr: cfg.Addr(),
Handler: r,
ReadTimeout: time.Duration(cfg.Server.ReadTimeoutMS) * time.Millisecond,
WriteTimeout: 0, // SSE streams must not be cut off by WriteTimeout
IdleTimeout: 120 * time.Second,
}
return &Server{httpSrv: srv}
}
func (s *Server) Start() error {
slog.Info("http server starting", "addr", s.httpSrv.Addr)
if err := s.httpSrv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpSrv.Shutdown(ctx)
}