From b311a355fd592cb1c83b5e08cde15747e6807a65 Mon Sep 17 00:00:00 2001 From: simon Date: Tue, 27 Oct 2026 23:05:10 +0100 Subject: [PATCH] Wire demo seed so TUI connects to Test and Website Relaunch. Dev seed now issues write tokens and writes local taskmaster meta under TASK_MASTER_HOME, so make run-dev can sync without manual setup. Co-authored-by: Cursor --- Makefile | 7 ++- README.md | 6 +- cmd/tm-server/main.go | 10 +++- internal/server/seed.go | 113 ++++++++++++++++++++++++++++++++++- internal/server/seed_test.go | 76 ++++++++++++++++++++++- 5 files changed, 201 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index b7bdc76..17762fa 100644 --- a/Makefile +++ b/Makefile @@ -45,12 +45,15 @@ run-dev: build TASK_MASTER_HOME=$(DEV_HOME) ./bin/$(BINARY) # Web test environment: SQLite demo against tm_server only (no YouTrack). -# Seeds 50 demo projects (Test + samples); login admin/admin (alice/alice as member). +# Seeds 50 demo projects; wires TUI projects Test + Website Relaunch with write tokens. +# Login admin/admin (alice/alice as member). Then: make run-dev → Y pull. run-server-dev: web-build build-server-go @rm -f $(DEV_HOME)/.local/share/task-master/task-master.db \ $(DEV_HOME)/.local/share/task-master/task-master.db-wal \ $(DEV_HOME)/.local/share/task-master/task-master.db-shm - @rm -rf $(DEV_HOME)/.local/share/task-master/projects/youtrack + @rm -rf $(DEV_HOME)/.local/share/task-master/projects/youtrack \ + "$(DEV_HOME)/.local/share/task-master/projects/Test" \ + "$(DEV_HOME)/.local/share/task-master/projects/Website Relaunch" TASK_MASTER_HOME=$(DEV_HOME) TM_ADMIN_USER=admin TM_ADMIN_PASSWORD=admin \ TM_SEED_DEMO=1 TASK_MASTER_SECRET=dev-secret \ ./bin/$(BINARY_SERVER) -listen :8080 diff --git a/README.md b/README.md index 3ba1349..b91c792 100644 --- a/README.md +++ b/README.md @@ -46,8 +46,9 @@ tm help ### Web-Server (`tm_server`) ```bash -# Dev/Test gegen lokales tm_server (kein YouTrack): seedet Projekt „Test“ + Samples +# Dev/Test gegen lokales tm_server (kein YouTrack): seedet Projekte + verdrahtet TUI make run-server-dev # :8080, Data unter ./.dev-home, Login admin/admin +make run-dev # TUI: Projekte „Test“ + „Website Relaunch“ schon mit Token connected # Produktion / manuell: export TM_ADMIN_USER=admin @@ -60,7 +61,8 @@ export TASK_MASTER_SECRET='long-random-secret' # verschlüsselt Remote-Tokens ``` UI: http://localhost:8080 — Dashboard, Board, Report, Members, Settings (Tokens), Users (Admin), Ticket-Zeit & History. -Das Test-Environment (`run-server-dev`) spricht nur mit diesem Backend; YouTrack ist dort nicht verdrahtet. +Das Test-Environment (`run-server-dev`) spricht nur mit diesem Backend; YouTrack ist dort nicht verdrahtet. +Beim Seed werden für **Test** und **Website Relaunch** Write-Tokens erzeugt und unter `.dev-home` als lokale TUI-Projekte (`backend=taskmaster`) hinterlegt — danach `make run-dev` und Sync (Y → Pull). Dev-Frontend (Vite, API-Proxy): `cd web && npm run dev` parallel zu `tm_server`. diff --git a/cmd/tm-server/main.go b/cmd/tm-server/main.go index c3cef4f..4eb3603 100644 --- a/cmd/tm-server/main.go +++ b/cmd/tm-server/main.go @@ -88,7 +88,7 @@ func bootstrapAdmin(db *sqlite.DB) error { } func runServer(listen, dbPath string, secure bool) error { - db, _, err := openDB(dbPath) + db, cfg, err := openDB(dbPath) if err != nil { return err } @@ -100,7 +100,10 @@ func runServer(listen, dbPath string, secure bool) error { srv.Secure = secure if strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "1") || strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "true") { - if err := srv.SeedDemo(); err != nil { + if err := srv.SeedDemo(server.SeedDemoOptions{ + ClientDataDir: cfg.DataDir, + BaseURL: server.SeedBaseURL(listen), + }); err != nil { return fmt.Errorf("seed: %w", err) } projects, _ := srv.Projects.List() @@ -183,6 +186,7 @@ Env: TASK_MASTER_HOME / TASK_MASTER_DATA_DIR same as tm TASK_MASTER_SECRET encrypts remote tokens TM_ADMIN_USER / TM_ADMIN_PASSWORD bootstrap first admin - TM_SEED_DEMO=1 seed local Test projects when DB empty (no YouTrack) + TM_SEED_DEMO=1 seed demo projects + wire TUI (Test, Website Relaunch) + TM_SEED_BASE_URL override base URL written into TUI meta.json `) } diff --git a/internal/server/seed.go b/internal/server/seed.go index fb80f0c..4602611 100644 --- a/internal/server/seed.go +++ b/internal/server/seed.go @@ -3,16 +3,31 @@ package server import ( "fmt" "os" + "path/filepath" "strings" "time" + "task/internal/config" + "task/internal/project" "task/internal/sqlite" "task/internal/ticket" ) +// SeedDemoOptions controls optional client wiring during demo seed. +type SeedDemoOptions struct { + // ClientDataDir, when set, writes local TUI projects (meta.json + tokens) + // for "Test" and "Website Relaunch" under that data directory. + ClientDataDir string + // BaseURL is the tm_server URL stored in client meta (default http://127.0.0.1:8080). + BaseURL string +} + +// tuiSeedProjects are server project names that get a write token and local TUI wiring. +var tuiSeedProjects = []string{"Test", "Website Relaunch"} + // SeedDemo populates sample users/projects/tickets when the DB has no projects yet. // No external backends (YouTrack etc.) — the web UI talks only to this tm_server. -func (s *Server) SeedDemo() error { +func (s *Server) SeedDemo(opts SeedDemoOptions) error { projects, err := s.Projects.List() if err != nil { return err @@ -58,7 +73,7 @@ func (s *Server) SeedDemo() error { {Title: "Board & Login prüfen", Desc: "Mit admin/admin einloggen, Board öffnen, Status ändern.\n\n- [ ] Login\n- [ ] Ticket editieren\n- [ ] Timer starten", Status: ticket.StatusInProgress}, {Title: "Zeit auf Ticket buchen", Desc: "Im Ticket-Detail Timer oder manuelle Minuten buchen, dann Report prüfen.", Status: ticket.StatusTodo}, {Title: "Members & Rechte", Desc: "alice als Member ist schon angelegt — Memberships-Tab ansehen.", Status: ticket.StatusTodo}, - {Title: "TUI anbinden", Desc: "Settings → Token (write) erzeugen → in `tm` Projekt-Edit backend=taskmaster eintragen (URL, Token, diese Project-ID).", Status: ticket.StatusTodo}, + {Title: "TUI Sync", Desc: "Lokal unter `.dev-home` schon mit Token verdrahtet — in `tm` (`make run-dev`) Pull/Push testen.", Status: ticket.StatusTodo}, {Title: "Seed abgeschlossen", Desc: "Beispiel-Ticket im Status DONE.", Status: ticket.StatusDone}, }, }, @@ -157,5 +172,99 @@ func (s *Server) SeedDemo() error { } fmt.Fprintf(os.Stderr, "seeded demo: %d projects, 2 groups (users %s + alice/alice), sync=none\n", len(demos), admin.Username) + + if dir := strings.TrimSpace(opts.ClientDataDir); dir != "" { + if err := s.wireDemoTUI(dir, opts.BaseURL, created, admin.ID); err != nil { + return fmt.Errorf("seed tui: %w", err) + } + } return nil } + +func (s *Server) wireDemoTUI(dataDir, baseURL string, created []struct{ ID, Name string }, adminID string) error { + baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/") + if baseURL == "" { + baseURL = "http://127.0.0.1:8080" + } + + byName := make(map[string]string, len(created)) + for _, c := range created { + byName[c.Name] = c.ID + } + + store := project.NewDirStore(dataDir) + wired := make([]string, 0, len(tuiSeedProjects)) + for _, name := range tuiSeedProjects { + remoteID, ok := byName[name] + if !ok { + return fmt.Errorf("missing seeded project %q", name) + } + tok, err := s.DB.CreateProjectToken(remoteID, "dev-tui", sqlite.TokenScopeWrite, adminID) + if err != nil { + return fmt.Errorf("token for %s: %w", name, err) + } + // Fresh local dir so stale tickets from a previous DB wipe cannot confuse sync. + _ = os.RemoveAll(filepath.Join(dataDir, "projects", name)) + meta := project.Meta{ + Backend: project.BackendTaskMaster, + TaskMaster: &project.TaskMaster{ + BaseURL: baseURL, + Token: tok.Token, + ProjectID: remoteID, + }, + } + if err := store.SaveMeta(name, meta); err != nil { + return fmt.Errorf("meta %s: %w", name, err) + } + wired = append(wired, name) + fmt.Fprintf(os.Stderr, "tui wired: %s → %s (project %s, token %s…)\n", + name, baseURL, remoteID, trimTokenPrefix(tok.Token)) + } + + if err := setActiveProjectIfSameDataDir(dataDir, tuiSeedProjects[0]); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "tui ready: %s (make run-dev → Y pull)\n", strings.Join(wired, ", ")) + return nil +} + +func trimTokenPrefix(tok string) string { + if len(tok) <= 10 { + return tok + } + return tok[:10] +} + +func setActiveProjectIfSameDataDir(dataDir, active string) error { + cfg, err := config.LoadOrCreate() + if err != nil { + return fmt.Errorf("config: %w", err) + } + if filepath.Clean(cfg.DataDir) != filepath.Clean(dataDir) { + return nil + } + cfg.ActiveProject = active + if err := config.Save(cfg); err != nil { + return fmt.Errorf("save config: %w", err) + } + return nil +} + +// SeedBaseURL derives the public base URL for seeded TUI meta from -listen / env. +func SeedBaseURL(listen string) string { + if u := strings.TrimSpace(os.Getenv("TM_SEED_BASE_URL")); u != "" { + return strings.TrimRight(u, "/") + } + host := strings.TrimSpace(listen) + if host == "" { + host = ":8080" + } + switch { + case strings.HasPrefix(host, "http://"), strings.HasPrefix(host, "https://"): + return strings.TrimRight(host, "/") + case strings.HasPrefix(host, ":"): + return "http://127.0.0.1" + host + default: + return "http://" + host + } +} diff --git a/internal/server/seed_test.go b/internal/server/seed_test.go index b6c9940..25642d3 100644 --- a/internal/server/seed_test.go +++ b/internal/server/seed_test.go @@ -1,9 +1,11 @@ package server import ( + "os" "path/filepath" "testing" + "task/internal/project" "task/internal/sqlite" ) @@ -17,7 +19,7 @@ func TestSeedDemo(t *testing.T) { t.Fatal(err) } srv := New(db) - if err := srv.SeedDemo(); err != nil { + if err := srv.SeedDemo(SeedDemoOptions{}); err != nil { t.Fatal(err) } projects, err := srv.Projects.List() @@ -55,7 +57,7 @@ func TestSeedDemo(t *testing.T) { t.Fatalf("expected no remote connections, got %v", conns) } // idempotent when projects already exist - if err := srv.SeedDemo(); err != nil { + if err := srv.SeedDemo(SeedDemoOptions{}); err != nil { t.Fatal(err) } projects2, _ := srv.Projects.List() @@ -63,3 +65,73 @@ func TestSeedDemo(t *testing.T) { t.Fatalf("re-seed changed project count") } } + +func TestSeedDemoWiresTUI(t *testing.T) { + home := t.TempDir() + t.Setenv("TASK_MASTER_HOME", home) + dataDir := filepath.Join(home, ".local", "share", "task-master") + if err := os.MkdirAll(dataDir, 0o755); err != nil { + t.Fatal(err) + } + + db, err := sqlite.Open(filepath.Join(dataDir, "seed.db"), []byte("s")) + if err != nil { + t.Fatal(err) + } + defer db.Close() + if _, err := db.CreateUser("admin", "admin", true); err != nil { + t.Fatal(err) + } + srv := New(db) + if err := srv.SeedDemo(SeedDemoOptions{ + ClientDataDir: dataDir, + BaseURL: "http://127.0.0.1:8080", + }); err != nil { + t.Fatal(err) + } + + store := project.NewDirStore(dataDir) + for _, name := range tuiSeedProjects { + meta, err := store.LoadMeta(name) + if err != nil { + t.Fatalf("LoadMeta %s: %v", name, err) + } + if meta.EffectiveBackend() != project.BackendTaskMaster { + t.Fatalf("%s backend=%q", name, meta.EffectiveBackend()) + } + if meta.TaskMaster == nil || !meta.TaskMaster.Configured() { + t.Fatalf("%s taskmaster not configured: %+v", name, meta.TaskMaster) + } + if meta.TaskMaster.BaseURL != "http://127.0.0.1:8080" { + t.Fatalf("baseURL=%q", meta.TaskMaster.BaseURL) + } + auth, err := db.ResolveBearerToken(meta.TaskMaster.Token) + if err != nil { + t.Fatalf("token resolve %s: %v", name, err) + } + if auth.ProjectID != meta.TaskMaster.ProjectID { + t.Fatalf("token project mismatch") + } + if !sqlite.TokenAllows(auth.Scope, sqlite.TokenScopeWrite) { + t.Fatalf("want write scope, got %q", auth.Scope) + } + tokens, err := db.ListProjectTokens(meta.TaskMaster.ProjectID) + if err != nil || len(tokens) != 1 || tokens[0].Name != "dev-tui" { + t.Fatalf("tokens=%v err=%v", tokens, err) + } + } +} + +func TestSeedBaseURL(t *testing.T) { + t.Setenv("TM_SEED_BASE_URL", "") + if got := SeedBaseURL(":8080"); got != "http://127.0.0.1:8080" { + t.Fatalf("got %q", got) + } + if got := SeedBaseURL("0.0.0.0:9000"); got != "http://0.0.0.0:9000" { + t.Fatalf("got %q", got) + } + t.Setenv("TM_SEED_BASE_URL", "http://localhost:8080/") + if got := SeedBaseURL(":9999"); got != "http://localhost:8080" { + t.Fatalf("env override got %q", got) + } +}