feat: initial scaffold of kanban_cpp v2 (issue 0130)

Frontend C++ ImGui (main.cpp + 4 paneles) + backend Go (HTTP + SQLite + fsnotify + SSE).
Reusa parse/scan/watch funcs del registry (issue 0130a).
This commit is contained in:
agent
2026-05-22 22:19:47 +02:00
commit 255e8dcf71
21 changed files with 2178 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"context"
"database/sql"
"embed"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"path/filepath"
"syscall"
"time"
)
//go:embed migrations/*.sql
var migrationsFS embed.FS
type Server struct {
db *sql.DB
issuesDir string
flowsDir string
hub *SSEHub
}
func main() {
port := flag.Int("port", 8487, "HTTP port")
dbPath := flag.String("db", "operations.db", "SQLite path")
registryRoot := flag.String("registry", "", "fn_registry root (default: auto-detect from cwd)")
flag.Parse()
root := *registryRoot
if root == "" {
root = detectRegistryRoot()
}
issuesDir := filepath.Join(root, "dev", "issues")
flowsDir := filepath.Join(root, "dev", "flows")
for _, d := range []string{issuesDir, flowsDir} {
if _, err := os.Stat(d); err != nil {
log.Fatalf("missing dir %s: %v", d, err)
}
}
db, err := openDB(*dbPath)
if err != nil {
log.Fatalf("openDB: %v", err)
}
defer db.Close()
if err := applyMigrations(db); err != nil {
log.Fatalf("applyMigrations: %v", err)
}
srv := &Server{
db: db,
issuesDir: issuesDir,
flowsDir: flowsDir,
hub: newSSEHub(),
}
if err := srv.ingestAll(); err != nil {
log.Fatalf("initial ingest: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go srv.watchLoop(ctx, srv.issuesDir, "issue")
go srv.watchLoop(ctx, srv.flowsDir, "flow")
mux := http.NewServeMux()
srv.registerRoutes(mux)
httpSrv := &http.Server{
Addr: fmt.Sprintf(":%d", *port),
Handler: withMiddleware(mux),
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
go func() {
log.Printf("kanban_cpp_backend listening on :%d (registry=%s)", *port, root)
if err := httpSrv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("listen: %v", err)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
log.Println("shutdown")
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelShutdown()
httpSrv.Shutdown(shutdownCtx)
}
func detectRegistryRoot() string {
wd, _ := os.Getwd()
for d := wd; d != "/" && d != "."; d = filepath.Dir(d) {
if _, err := os.Stat(filepath.Join(d, "registry.db")); err == nil {
return d
}
}
log.Fatalf("could not auto-detect fn_registry root from %s", wd)
return ""
}