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
+120
View File
@@ -0,0 +1,120 @@
package sqlite
import (
"database/sql"
"encoding/json"
"fmt"
"time"
)
// Revision is a versioned snapshot of an entity.
type Revision struct {
ID int64 `json:"id"`
EntityType string `json:"entityType"`
EntityID string `json:"entityId"`
ProjectID string `json:"projectId"`
Rev int `json:"rev"`
At time.Time `json:"at"`
ActorUserID string `json:"actorUserId,omitempty"`
Op string `json:"op"`
Snapshot json.RawMessage `json:"snapshot"`
}
// AppendRevision stores a new revision snapshot.
func (db *DB) AppendRevision(entityType, entityID, projectID, op, actorUserID string, snapshot any) (Revision, error) {
data, err := json.Marshal(snapshot)
if err != nil {
return Revision{}, err
}
var maxRev sql.NullInt64
_ = db.SQL.QueryRow(`SELECT MAX(rev) FROM revisions WHERE entity_type = ? AND entity_id = ?`,
entityType, entityID).Scan(&maxRev)
rev := 1
if maxRev.Valid {
rev = int(maxRev.Int64) + 1
}
at := time.Now().UTC()
var actor any
if actorUserID != "" {
actor = actorUserID
}
res, err := db.SQL.Exec(`
INSERT INTO revisions(entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json)
VALUES(?,?,?,?,?,?,?,?)`,
entityType, entityID, projectID, rev, formatTime(at), actor, op, string(data))
if err != nil {
return Revision{}, err
}
id, _ := res.LastInsertId()
return Revision{
ID: id,
EntityType: entityType,
EntityID: entityID,
ProjectID: projectID,
Rev: rev,
At: at,
ActorUserID: actorUserID,
Op: op,
Snapshot: data,
}, nil
}
// ListRevisions returns revisions for an entity, newest first.
func (db *DB) ListRevisions(entityType, entityID string) ([]Revision, error) {
rows, err := db.SQL.Query(`
SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json
FROM revisions WHERE entity_type = ? AND entity_id = ?
ORDER BY rev DESC`, entityType, entityID)
if err != nil {
return nil, err
}
defer rows.Close()
return scanRevisions(rows)
}
// GetRevision returns one revision by id.
func (db *DB) GetRevision(id int64) (Revision, error) {
row := db.SQL.QueryRow(`
SELECT id, entity_type, entity_id, project_id, rev, at, actor_user_id, op, snapshot_json
FROM revisions WHERE id = ?`, id)
var r Revision
var at string
var actor sql.NullString
var snap string
err := row.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap)
if err == sql.ErrNoRows {
return Revision{}, fmt.Errorf("revision %d: not found", id)
}
if err != nil {
return Revision{}, err
}
r.At = parseTime(at)
if actor.Valid {
r.ActorUserID = actor.String
}
r.Snapshot = json.RawMessage(snap)
return r, nil
}
func scanRevisions(rows *sql.Rows) ([]Revision, error) {
var out []Revision
for rows.Next() {
var r Revision
var at string
var actor sql.NullString
var snap string
if err := rows.Scan(&r.ID, &r.EntityType, &r.EntityID, &r.ProjectID, &r.Rev, &at, &actor, &r.Op, &snap); err != nil {
return nil, err
}
r.At = parseTime(at)
if actor.Valid {
r.ActorUserID = actor.String
}
r.Snapshot = json.RawMessage(snap)
out = append(out, r)
}
if out == nil {
out = []Revision{}
}
return out, rows.Err()
}