claude-web-chat/main.go
djuka 3122c5cba9
Some checks failed
Tests / unit-tests (push) Failing after 22s
Pojednostavljen chat na jedan terminal, dodata notifikacija kad Claude završi
Uklonjen multi-tab sistem — sada jedna PTY sesija po stranici.
Dodat idle detection: status "Završeno", flash animacija, browser
notifikacija i treptanje naslova kad je tab u pozadini.
CSS premešten iz inline stilova u style.css.
Dodat /api/projects endpoint i testovi za PTY sesije.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 07:20:55 +00:00

265 lines
7.5 KiB
Go

package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"path/filepath"
)
var (
cfg *Config
templates *TemplateRenderer
sessionMgr *SessionManager
ptyMgr *PTYSessionManager
)
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)
ptyMgr = NewPTYSessionManager()
defer ptyMgr.Stop()
wsHandler := NewTerminalHandler(ptyMgr)
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("POST /projects/create", AuthMiddleware(sessionMgr, http.HandlerFunc(handleCreateProject)))
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)))
mux.Handle("GET /api/projects", AuthMiddleware(sessionMgr, http.HandlerFunc(handleProjectsAPI)))
mux.Handle("GET /change-password", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChangePasswordPage)))
mux.Handle("POST /change-password", AuthMiddleware(sessionMgr, http.HandlerFunc(handleChangePassword)))
// 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 || !cfg.CheckPassword(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 handleCreateProject(w http.ResponseWriter, r *http.Request) {
name := r.FormValue("name")
if err := CreateProject(cfg.ProjectsPath, name); err != nil {
// AJAX request — return JSON error
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
json.NewEncoder(w).Encode(map[string]string{"error": err.Error()})
return
}
projects, _ := ListProjects(cfg.ProjectsPath)
data := map[string]any{
"Projects": projects,
"ProjectsPath": cfg.ProjectsPath,
"Error": err.Error(),
}
w.WriteHeader(http.StatusBadRequest)
templates.Render(w, "projects.html", data)
return
}
// AJAX request — return JSON success
if r.Header.Get("X-Requested-With") == "XMLHttpRequest" {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"name": name})
return
}
http.Redirect(w, r, "/projects", http.StatusSeeOther)
}
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
}
projects, err := ListProjects(cfg.ProjectsPath)
if err != nil {
log.Printf("ListProjects error: %v", err)
projects = nil
}
data := map[string]any{
"Project": project,
"ProjectDir": projectDir,
"Files": files,
"Projects": projects,
}
templates.Render(w, "chat.html", data)
}
func handleChangePasswordPage(w http.ResponseWriter, r *http.Request) {
templates.Render(w, "change-password.html", map[string]string{"Error": "", "Success": ""})
}
func handleChangePassword(w http.ResponseWriter, r *http.Request) {
currentPassword := r.FormValue("current_password")
newPassword := r.FormValue("new_password")
confirmPassword := r.FormValue("confirm_password")
data := map[string]string{"Error": "", "Success": ""}
if !cfg.CheckPassword(currentPassword) {
data["Error"] = "Pogrešna trenutna lozinka"
w.WriteHeader(http.StatusBadRequest)
templates.Render(w, "change-password.html", data)
return
}
if len(newPassword) < 6 {
data["Error"] = "Nova lozinka mora imati najmanje 6 karaktera"
w.WriteHeader(http.StatusBadRequest)
templates.Render(w, "change-password.html", data)
return
}
if newPassword != confirmPassword {
data["Error"] = "Nova lozinka i potvrda se ne poklapaju"
w.WriteHeader(http.StatusBadRequest)
templates.Render(w, "change-password.html", data)
return
}
if err := cfg.SetPassword(newPassword); err != nil {
log.Printf("SetPassword error: %v", err)
data["Error"] = "Greška pri čuvanju lozinke"
w.WriteHeader(http.StatusInternalServerError)
templates.Render(w, "change-password.html", data)
return
}
data["Success"] = "Lozinka je uspešno promenjena"
templates.Render(w, "change-password.html", data)
}
func handleProjectsAPI(w http.ResponseWriter, r *http.Request) {
projects, err := ListProjects(cfg.ProjectsPath)
if err != nil {
http.Error(w, "error listing projects", http.StatusInternalServerError)
return
}
type projectItem struct {
Name string `json:"name"`
Path string `json:"path"`
}
items := make([]projectItem, len(projects))
for i, p := range projects {
items[i] = projectItem{Name: p.Name, Path: p.Path}
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(items)
}
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,
})
}