Add tm_server with SQLite, Svelte UI, and task-master TUI sync.

Introduces the web backend (auth, RBAC, project tokens, revisions),
YouTrack-or-taskmaster backend choice in the TUI, and a first-pass
embedded Svelte UI. The frontend is functional but needs a major rework.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 22:23:11 +01:00
co-authored by Cursor
parent 98d2253f44
commit 4adfcdb95a
67 changed files with 7188 additions and 129 deletions
+98
View File
@@ -0,0 +1,98 @@
package sqlite
import (
"path/filepath"
"testing"
"time"
"task/internal/ticket"
)
func TestStoresRoundTrip(t *testing.T) {
db, err := Open(filepath.Join(t.TempDir(), "test.db"), []byte("test-secret"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
ps := NewProjectStore(db)
if err := ps.Ensure("inbox"); err != nil {
t.Fatal(err)
}
ts := NewTicketStore(db)
created, err := ts.Create(ticket.Ticket{Title: "Hello", ProjectID: "inbox", Status: ticket.StatusTodo})
if err != nil {
t.Fatal(err)
}
got, err := ts.Get(created.ID)
if err != nil {
t.Fatal(err)
}
if got.Title != "Hello" {
t.Fatalf("title=%q", got.Title)
}
got.Title = "Updated"
if err := ts.Update(got); err != nil {
t.Fatal(err)
}
if err := ts.MarkSynced(got.ID); err != nil {
t.Fatal(err)
}
synced, _ := ts.Get(got.ID)
if synced.SyncedAt.IsZero() || !synced.SyncedAt.Equal(synced.UpdatedAt) {
t.Fatalf("syncedAt=%v updatedAt=%v", synced.SyncedAt, synced.UpdatedAt)
}
timeStore := NewTimeStore(db)
_, err = timeStore.Create(ticket.TimeEntry{
TicketID: created.ID,
ProjectID: "inbox",
Duration: time.Hour,
Source: ticket.SourceManual,
})
if err != nil {
t.Fatal(err)
}
sum, err := timeStore.SumByTicket(created.ID)
if err != nil || sum != time.Hour {
t.Fatalf("sum=%v err=%v", sum, err)
}
u, err := db.CreateUser("admin", "secret", true)
if err != nil {
t.Fatal(err)
}
token, _, err := db.CreateSession(u.ID, time.Hour)
if err != nil {
t.Fatal(err)
}
me, err := db.UserFromSession(token)
if err != nil || me.Username != "admin" {
t.Fatalf("session user=%v err=%v", me, err)
}
if err := db.SetMember("inbox", u.ID, RoleOwner); err != nil {
t.Fatal(err)
}
role, _ := db.MemberRole("inbox", u.ID)
if role != RoleOwner {
t.Fatalf("role=%s", role)
}
_, err = db.AppendRevision("ticket", created.ID, "inbox", "update", u.ID, synced)
if err != nil {
t.Fatal(err)
}
revs, err := db.ListRevisions("ticket", created.ID)
if err != nil || len(revs) != 1 {
t.Fatalf("revs=%v err=%v", revs, err)
}
conn, err := db.CreateConnection("YT", "youtrack", "https://yt.example", "instance", "tok-123", u.ID)
if err != nil {
t.Fatal(err)
}
loaded, err := db.GetConnection(conn.ID)
if err != nil || loaded.Token != "tok-123" {
t.Fatalf("token=%q err=%v", loaded.Token, err)
}
}