Restructure the UI into top-level tabs and a scrollable project tree, and persist optional project groups so demos and settings can organize many projects. Co-authored-by: Cursor <cursoragent@cursor.com>
162 lines
4.8 KiB
Go
162 lines
4.8 KiB
Go
package server
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"task/internal/sqlite"
|
|
"task/internal/ticket"
|
|
)
|
|
|
|
// 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 {
|
|
projects, err := s.Projects.List()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(projects) > 0 {
|
|
return nil
|
|
}
|
|
|
|
users, err := s.DB.ListUsers()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(users) == 0 {
|
|
return fmt.Errorf("seed: no users (bootstrap admin first)")
|
|
}
|
|
admin := users[0]
|
|
for _, u := range users {
|
|
if u.IsAdmin {
|
|
admin = u
|
|
break
|
|
}
|
|
}
|
|
|
|
member, err := s.DB.CreateUser("alice", "alice", false)
|
|
if err != nil {
|
|
member, err = s.DB.GetUserByUsername("alice")
|
|
if err != nil {
|
|
return fmt.Errorf("seed alice: %w", err)
|
|
}
|
|
}
|
|
|
|
type seedTicket struct {
|
|
Title, Desc string
|
|
Status ticket.Status
|
|
}
|
|
demos := []struct {
|
|
Name string
|
|
Tickets []seedTicket
|
|
}{
|
|
{
|
|
Name: "Test",
|
|
Tickets: []seedTicket{
|
|
{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: "Seed abgeschlossen", Desc: "Beispiel-Ticket im Status DONE.", Status: ticket.StatusDone},
|
|
},
|
|
},
|
|
{
|
|
Name: "Website Relaunch",
|
|
Tickets: []seedTicket{
|
|
{Title: "Landing Hero entwerfen", Desc: "Dunkles Layout, klare CTAs.\n\n- [ ] Moodboard\n- [ ] Wireframe", Status: ticket.StatusInProgress},
|
|
{Title: "CMS-Inhalte migrieren", Desc: "Alte Seiten nach Markdown überführen.", Status: ticket.StatusTodo},
|
|
{Title: "Impressum & Datenschutz", Desc: "Rechtstexte finalisieren.", Status: ticket.StatusDone},
|
|
},
|
|
},
|
|
{
|
|
Name: "API Platform",
|
|
Tickets: []seedTicket{
|
|
{Title: "Auth-Middleware härten", Desc: "Session-Cookies dokumentieren.", Status: ticket.StatusInProgress},
|
|
{Title: "Rate-Limits festlegen", Desc: "Pro User und pro IP.", Status: ticket.StatusTodo},
|
|
{Title: "Health-Check", Desc: "Bereits live.", Status: ticket.StatusDone},
|
|
},
|
|
},
|
|
}
|
|
for i := len(demos) + 1; i <= 50; i++ {
|
|
demos = append(demos, struct {
|
|
Name string
|
|
Tickets []seedTicket
|
|
}{
|
|
Name: fmt.Sprintf("Sample %02d", i),
|
|
Tickets: []seedTicket{
|
|
{Title: "Starter ticket", Desc: "Seed-Platzhalter.", Status: ticket.StatusTodo},
|
|
},
|
|
})
|
|
}
|
|
|
|
now := time.Now().UTC()
|
|
created := make([]struct{ ID, Name string }, 0, len(demos))
|
|
for _, d := range demos {
|
|
p, err := s.Projects.Create("", d.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
created = append(created, struct{ ID, Name string }{p.ID, p.Name})
|
|
_ = s.DB.SetMember(p.ID, admin.ID, sqlite.RoleOwner)
|
|
_ = s.DB.SetMember(p.ID, member.ID, sqlite.RoleMember)
|
|
// Explicit local-only sync (no remote backend in the test environment).
|
|
_ = s.Projects.SetSyncBackend(sqlite.SyncBackend{
|
|
ProjectID: p.ID,
|
|
Kind: "none",
|
|
})
|
|
|
|
var firstID string
|
|
for i, st := range d.Tickets {
|
|
t, err := s.Tickets.Create(ticket.Ticket{
|
|
Title: st.Title,
|
|
Description: st.Desc,
|
|
Status: st.Status,
|
|
ProjectID: p.ID,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = s.DB.AppendRevision("ticket", t.ID, p.ID, "seed", admin.ID, t)
|
|
if i == 0 {
|
|
firstID = t.ID
|
|
}
|
|
}
|
|
if firstID != "" {
|
|
_, err := s.Times.Create(ticket.TimeEntry{
|
|
TicketID: firstID,
|
|
ProjectID: p.ID,
|
|
Start: now.Add(-2 * time.Hour),
|
|
End: now.Add(-90 * time.Minute),
|
|
Duration: 30 * time.Minute,
|
|
Source: ticket.SourceManual,
|
|
Note: "seed",
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
kunden, err := s.DB.CreateProjectGroup("Kunden")
|
|
if err != nil {
|
|
return fmt.Errorf("seed group Kunden: %w", err)
|
|
}
|
|
intern, err := s.DB.CreateProjectGroup("Intern")
|
|
if err != nil {
|
|
return fmt.Errorf("seed group Intern: %w", err)
|
|
}
|
|
for _, c := range created {
|
|
switch {
|
|
case c.Name == "Website Relaunch" || c.Name == "API Platform" || strings.HasPrefix(c.Name, "Sample 0"):
|
|
_ = s.Projects.SetGroup(c.ID, kunden.ID)
|
|
case c.Name == "Test" || strings.HasPrefix(c.Name, "Sample 1") || strings.HasPrefix(c.Name, "Sample 2"):
|
|
_ = s.Projects.SetGroup(c.ID, intern.ID)
|
|
}
|
|
}
|
|
|
|
fmt.Fprintf(os.Stderr, "seeded demo: %d projects, 2 groups (users %s + alice/alice), sync=none\n", len(demos), admin.Username)
|
|
return nil
|
|
}
|