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 <cursoragent@cursor.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -61,6 +62,7 @@ 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.
|
||||
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`.
|
||||
|
||||
|
||||
@@ -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
|
||||
`)
|
||||
}
|
||||
|
||||
+111
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user