Assign readable ticket keys from project code and sequence.

Replace UUID-looking sync labels in TUI and web with keys like TAS-1,
while keeping the remote UUID for task-master matching.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-28 00:09:14 +01:00
co-authored by Cursor
parent 26a3723a06
commit efeb4a74a8
14 changed files with 535 additions and 28 deletions
+44 -10
View File
@@ -25,7 +25,7 @@ func NewProjectStore(db *DB) *ProjectStore {
func scanProject(scanner interface{ Scan(dest ...any) error }) (project.Project, error) {
var p project.Project
var groupID sql.NullString
if err := scanner.Scan(&p.ID, &p.Name, &groupID); err != nil {
if err := scanner.Scan(&p.ID, &p.Name, &p.Code, &groupID); err != nil {
return project.Project{}, err
}
if groupID.Valid {
@@ -34,9 +34,11 @@ func scanProject(scanner interface{ Scan(dest ...any) error }) (project.Project,
return p, nil
}
const projectCols = `id, name, code, group_id`
// List returns all projects sorted by name.
func (s *ProjectStore) List() ([]project.Project, error) {
rows, err := s.db.SQL.Query(`SELECT id, name, group_id FROM projects ORDER BY name COLLATE NOCASE`)
rows, err := s.db.SQL.Query(`SELECT ` + projectCols + ` FROM projects ORDER BY name COLLATE NOCASE`)
if err != nil {
return nil, fmt.Errorf("list projects: %w", err)
}
@@ -55,19 +57,46 @@ func (s *ProjectStore) List() ([]project.Project, error) {
return out, rows.Err()
}
func (s *ProjectStore) usedCodes() (map[string]bool, error) {
rows, err := s.db.SQL.Query(`SELECT code FROM projects WHERE code != ''`)
if err != nil {
return nil, err
}
defer rows.Close()
used := map[string]bool{}
for rows.Next() {
var c string
if err := rows.Scan(&c); err != nil {
return nil, err
}
used[c] = true
}
return used, rows.Err()
}
// Ensure creates the project if missing.
func (s *ProjectStore) Ensure(id string) error {
id = strings.TrimSpace(id)
if id == "" {
return fmt.Errorf("project id is required")
}
used, err := s.usedCodes()
if err != nil {
return err
}
code := nextUniqueCode(id, used)
now := formatTime(time.Now().UTC())
_, err := s.db.SQL.Exec(`
INSERT INTO projects(id, name, created_at, updated_at) VALUES(?, ?, ?, ?)
ON CONFLICT(id) DO NOTHING`, id, id, now, now)
_, err = s.db.SQL.Exec(`
INSERT INTO projects(id, name, code, created_at, updated_at) VALUES(?, ?, ?, ?, ?)
ON CONFLICT(id) DO NOTHING`, id, id, code, now, now)
if err != nil {
return fmt.Errorf("ensure project %s: %w", id, err)
}
var existing string
_ = s.db.SQL.QueryRow(`SELECT code FROM projects WHERE id = ?`, id).Scan(&existing)
if existing == "" {
return s.db.backfillProjectCodes()
}
return nil
}
@@ -81,13 +110,18 @@ func (s *ProjectStore) Create(id, name string) (project.Project, error) {
if id == "" {
id = uuid.NewString()
}
used, err := s.usedCodes()
if err != nil {
return project.Project{}, err
}
code := nextUniqueCode(name, used)
now := formatTime(time.Now().UTC())
_, err := s.db.SQL.Exec(`INSERT INTO projects(id, name, created_at, updated_at, group_id) VALUES(?, ?, ?, ?, NULL)`,
id, name, now, now)
_, err = s.db.SQL.Exec(`INSERT INTO projects(id, name, code, created_at, updated_at, group_id) VALUES(?, ?, ?, ?, ?, NULL)`,
id, name, code, now, now)
if err != nil {
return project.Project{}, fmt.Errorf("create project: %w", err)
}
return project.Project{ID: id, Name: name}, nil
return project.Project{ID: id, Name: name, Code: code}, nil
}
// SetGroup assigns a project to a group. Empty groupID clears the assignment.
@@ -151,7 +185,7 @@ func (s *ProjectStore) Delete(id string) error {
// Get returns one project.
func (s *ProjectStore) Get(id string) (project.Project, error) {
p, err := scanProject(s.db.SQL.QueryRow(`SELECT id, name, group_id FROM projects WHERE id = ?`, id))
p, err := scanProject(s.db.SQL.QueryRow(`SELECT `+projectCols+` FROM projects WHERE id = ?`, id))
if err == sql.ErrNoRows {
return project.Project{}, fmt.Errorf("project %s: not found", id)
}
@@ -291,7 +325,7 @@ func (s *ProjectStore) ListIDsForUser(userID string, isAdmin bool) ([]project.Pr
return s.List()
}
rows, err := s.db.SQL.Query(`
SELECT p.id, p.name, p.group_id FROM projects p
SELECT p.id, p.name, p.code, p.group_id FROM projects p
INNER JOIN project_members m ON m.project_id = p.id
WHERE m.user_id = ?
ORDER BY p.name COLLATE NOCASE`, userID)