claude-web-chat/main.go
djuka 3283888738
All checks were successful
Tests / unit-tests (push) Successful in 51s
Inicijalna implementacija Claude Web Chat (Faza 1 - CLI mod)
- Login sa session cookie autentifikacijom
- Lista projekata iz filesystem-a
- Chat sa Claude CLI preko WebSocket-a
- Streaming NDJSON parsiranje iz CLI stdout-a
- Sesija zivi nezavisno od browsera (reconnect replay)
- Sidebar sa .md fajlovima i markdown renderovanjem
- Dark tema, htmx + Go templates
- 47 unit testova

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 05:03:40 +00:00

157 lines
4.0 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
)
var (
cfg *Config
templates *TemplateRenderer
sessionMgr *SessionManager
chatMgr *ChatSessionManager
)
func main() {
var err error
cfg, err = LoadConfig("config.json")
if err != nil {
log.Fatalf("Config: %v", err)
}
templates, err = NewTemplateRenderer("templates")
if err != nil {
log.Fatalf("Templates: %v", err)
}
sessionMgr = NewSessionManager(cfg.SessionSecret)
chatMgr = NewChatSessionManager()
defer chatMgr.Stop()
wsHandler := NewWSHandler(chatMgr)
mux := http.NewServeMux()
// Static files
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// Public routes
mux.HandleFunc("GET /login", handleLoginPage)
mux.HandleFunc("POST /login", handleLogin)
mux.HandleFunc("GET /logout", handleLogout)
// Protected routes
mux.Handle("GET /projects", AuthMiddleware(sessionMgr, http.HandlerFunc(handleProjects)))
mux.Handle("GET /chat/{project}", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChat)))
mux.Handle("GET /ws", AuthMiddleware(sessionMgr, wsHandler))
mux.Handle("GET /api/file", AuthMiddleware(sessionMgr, http.HandlerFunc(handleFileAPI)))
// Root redirect (exact match only)
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/projects", http.StatusSeeOther)
})
addr := fmt.Sprintf(":%d", cfg.Port)
log.Printf("Claude Web Chat pokrenut na http://0.0.0.0%s (mod: %s)", addr, cfg.Mode)
log.Fatal(http.ListenAndServe(addr, mux))
}
func handleLoginPage(w http.ResponseWriter, r *http.Request) {
// If already logged in, redirect
if cookie, err := r.Cookie(sessionCookieName); err == nil {
if sessionMgr.Get(cookie.Value) != nil {
http.Redirect(w, r, "/projects", http.StatusSeeOther)
return
}
}
templates.Render(w, "login.html", map[string]string{"Error": ""})
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
username := r.FormValue("username")
password := r.FormValue("password")
if username != cfg.Username || password != cfg.Password {
w.WriteHeader(http.StatusUnauthorized)
templates.Render(w, "login.html", map[string]string{"Error": "Pogrešno korisničko ime ili lozinka"})
return
}
sess := sessionMgr.Create(username)
SetSessionCookie(w, sess)
http.Redirect(w, r, "/projects", http.StatusSeeOther)
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if cookie, err := r.Cookie(sessionCookieName); err == nil {
sessionMgr.Delete(cookie.Value)
}
ClearSessionCookie(w)
http.Redirect(w, r, "/login", http.StatusSeeOther)
}
func handleProjects(w http.ResponseWriter, r *http.Request) {
projects, err := ListProjects(cfg.ProjectsPath)
if err != nil {
log.Printf("ListProjects error: %v", err)
http.Error(w, "Greška pri čitanju projekata", http.StatusInternalServerError)
return
}
data := map[string]any{
"Projects": projects,
"ProjectsPath": cfg.ProjectsPath,
}
templates.Render(w, "projects.html", data)
}
func handleChat(w http.ResponseWriter, r *http.Request) {
project := r.PathValue("project")
if project == "" {
http.Redirect(w, r, "/projects", http.StatusSeeOther)
return
}
projectDir := filepath.Join(cfg.ProjectsPath, project)
files, err := ListMarkdownFiles(projectDir)
if err != nil {
log.Printf("ListMarkdownFiles error: %v", err)
files = nil
}
data := map[string]any{
"Project": project,
"ProjectDir": projectDir,
"Files": files,
}
templates.Render(w, "chat.html", data)
}
func handleFileAPI(w http.ResponseWriter, r *http.Request) {
project := r.URL.Query().Get("project")
relPath := r.URL.Query().Get("path")
if project == "" || relPath == "" {
http.Error(w, "missing params", http.StatusBadRequest)
return
}
projectDir := filepath.Join(cfg.ProjectsPath, project)
htmlContent, err := RenderMarkdownFile(projectDir, relPath)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"name": relPath,
"html": htmlContent,
})
}