Files
task-master/cmd/tm-server/main.go
T
skrueckenandCursor 6e6b13270a Add ticket file attachments with simple in-browser Excel editing.
Store uploads on disk under DataDir and expose them in the web ticket view so .xlsx files can be previewed and saved as cell values without YouTrack or TUI sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 23:53:17 +01:00

194 lines
4.7 KiB
Go

package main
import (
"flag"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"task/internal/config"
"task/internal/server"
"task/internal/sqlite"
"task/internal/version"
)
func main() {
listen := flag.String("listen", ":8080", "HTTP listen address")
dbPath := flag.String("db", "", "SQLite database path (default: <dataDir>/task-master.db)")
secure := flag.Bool("secure", false, "Set Secure flag on session cookies")
flag.Parse()
args := flag.Args()
if len(args) > 0 {
switch args[0] {
case "version", "--version", "-v":
fmt.Printf("tm_server %s\n", version.BuildVersion)
return
case "help", "--help", "-h":
printHelp()
return
case "import-json":
if err := runImportJSON(args[1:]); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
return
default:
fmt.Fprintf(os.Stderr, "unknown command: %s\n", args[0])
printHelp()
os.Exit(1)
}
}
if err := runServer(*listen, *dbPath, *secure); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
os.Exit(1)
}
}
func openDB(dbPath string) (*sqlite.DB, *config.Config, error) {
cfg, err := config.LoadOrCreate()
if err != nil {
return nil, nil, fmt.Errorf("config: %w", err)
}
path := dbPath
if path == "" {
path = filepath.Join(cfg.DataDir, "task-master.db")
}
secret := []byte(os.Getenv("TASK_MASTER_SECRET"))
db, err := sqlite.Open(path, secret)
if err != nil {
return nil, nil, err
}
return db, cfg, nil
}
func bootstrapAdmin(db *sqlite.DB) error {
n, err := db.CountUsers()
if err != nil {
return err
}
if n > 0 {
return nil
}
user := strings.TrimSpace(os.Getenv("TM_ADMIN_USER"))
pass := os.Getenv("TM_ADMIN_PASSWORD")
if user == "" || pass == "" {
fmt.Fprintln(os.Stderr, "no users in database; set TM_ADMIN_USER and TM_ADMIN_PASSWORD to create the first admin")
return nil
}
u, err := db.CreateUser(user, pass, true)
if err != nil {
return err
}
fmt.Fprintf(os.Stderr, "created bootstrap admin %q (%s)\n", u.Username, u.ID)
return nil
}
func runServer(listen, dbPath string, secure bool) error {
db, cfg, err := openDB(dbPath)
if err != nil {
return err
}
defer db.Close()
if err := bootstrapAdmin(db); err != nil {
return err
}
srv := server.New(db)
srv.DataDir = cfg.DataDir
srv.Secure = secure
if strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "1") ||
strings.EqualFold(os.Getenv("TM_SEED_DEMO"), "true") {
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()
fmt.Fprintf(os.Stderr, "demo seed ready (%d projects; login admin/admin, alice/alice)\n", len(projects))
}
fmt.Fprintf(os.Stderr, "tm_server %s listening on %s\n", version.BuildVersion, listen)
return http.ListenAndServe(listen, srv.Handler())
}
func runImportJSON(args []string) error {
fs := flag.NewFlagSet("import-json", flag.ExitOnError)
dataDir := fs.String("data", "", "JSON DataDir to import (default from config)")
dbPath := fs.String("db", "", "SQLite database path")
owner := fs.String("owner", "", "username to own imported projects")
_ = fs.Parse(args)
db, cfg, err := openDB(*dbPath)
if err != nil {
return err
}
defer db.Close()
if err := bootstrapAdmin(db); err != nil {
return err
}
dir := *dataDir
if dir == "" {
dir = cfg.DataDir
}
var ownerID string
if *owner != "" {
u, err := db.GetUserByUsername(*owner)
if err != nil {
return fmt.Errorf("owner: %w", err)
}
ownerID = u.ID
} else {
users, err := db.ListUsers()
if err != nil {
return err
}
if len(users) == 0 {
return fmt.Errorf("no users; create admin first (TM_ADMIN_USER/TM_ADMIN_PASSWORD)")
}
for _, u := range users {
if u.IsAdmin {
ownerID = u.ID
break
}
}
if ownerID == "" {
ownerID = users[0].ID
}
}
srv := server.New(db)
fmt.Fprintf(os.Stderr, "importing from %s …\n", dir)
if err := srv.ImportJSONDataDir(dir, ownerID); err != nil {
return err
}
fmt.Fprintln(os.Stderr, "import done")
return nil
}
func printHelp() {
fmt.Print(`tm_server — task-master web server
Usage:
tm_server [flags]
tm_server import-json [-data DIR] [-db PATH] [-owner USER]
tm_server version
tm_server help
Flags:
-listen ADDR HTTP listen address (default :8080)
-db PATH SQLite path (default <dataDir>/task-master.db)
-secure Set Secure cookie flag
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 demo projects + wire TUI (Test, Website Relaunch)
TM_SEED_BASE_URL override base URL written into TUI meta.json
`)
}