mirror of
https://github.com/alibaba/open-code-review.git
synced 2026-07-09 17:28:58 +00:00
166 lines
3.9 KiB
Go
166 lines
3.9 KiB
Go
package viewer
|
|
|
|
import (
|
|
"embed"
|
|
"fmt"
|
|
"html/template"
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
//go:embed templates/*.html static/style.css
|
|
var assets embed.FS
|
|
|
|
func StartServer(addr string) error {
|
|
root, err := SessionsRoot()
|
|
if err != nil {
|
|
return fmt.Errorf("resolve sessions root: %w", err)
|
|
}
|
|
|
|
mux := http.NewServeMux()
|
|
|
|
// Static assets (must be registered before "/" catch-all)
|
|
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS()))))
|
|
|
|
// Routes
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
handleRepos(w, r, root)
|
|
})
|
|
mux.HandleFunc("/r/{repo}", func(w http.ResponseWriter, r *http.Request) {
|
|
repo := r.PathValue("repo")
|
|
if strings.Contains(repo, "..") || strings.Contains(repo, "/") {
|
|
http.Error(w, "invalid repo path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
handleSessions(w, r, root, repo)
|
|
})
|
|
mux.HandleFunc("/r/{repo}/{sessionID}", func(w http.ResponseWriter, r *http.Request) {
|
|
repo := r.PathValue("repo")
|
|
sid := r.PathValue("sessionID")
|
|
if strings.Contains(repo, "..") || strings.Contains(sid, "..") {
|
|
http.Error(w, "invalid path", http.StatusBadRequest)
|
|
return
|
|
}
|
|
handleSession(w, r, root, repo, sid)
|
|
})
|
|
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: mux,
|
|
}
|
|
|
|
fmt.Printf("\nOpen browser: http://%s\n", addr)
|
|
return srv.ListenAndServe()
|
|
}
|
|
|
|
var cstZone = func() *time.Location {
|
|
loc, err := time.LoadLocation("Asia/Shanghai")
|
|
if err != nil {
|
|
loc = time.FixedZone("CST", 8*60*60)
|
|
}
|
|
return loc
|
|
}()
|
|
|
|
func formatTime(t time.Time) string {
|
|
return t.In(cstZone).Format("2006-01-02 15:04")
|
|
}
|
|
|
|
func parseTemplate(name string) (*template.Template, error) {
|
|
funcMap := template.FuncMap{
|
|
"formatDuration": formatDuration,
|
|
"formatTime": formatTime,
|
|
"truncate": truncateText,
|
|
"add": func(a, b int) int { return a + b },
|
|
"cardCount": func(tasks map[TaskType][]*TaskCard) int {
|
|
n := 0
|
|
for _, cards := range tasks {
|
|
n += len(cards)
|
|
}
|
|
return n
|
|
},
|
|
"taskTypeClass": func(tt TaskType) string {
|
|
switch tt {
|
|
case PlanTask:
|
|
return "task-plan"
|
|
case MainTask:
|
|
return "task-main"
|
|
case MemoryCompressionTask:
|
|
return "task-memory"
|
|
default:
|
|
return "task-default"
|
|
}
|
|
},
|
|
"orderedTasks": func(tasks map[TaskType][]*TaskCard) []struct {
|
|
Type TaskType
|
|
Cards []*TaskCard
|
|
} {
|
|
order := []TaskType{PlanTask, MainTask, MemoryCompressionTask}
|
|
var result []struct {
|
|
Type TaskType
|
|
Cards []*TaskCard
|
|
}
|
|
for _, tt := range order {
|
|
if cards, ok := tasks[tt]; ok {
|
|
result = append(result, struct {
|
|
Type TaskType
|
|
Cards []*TaskCard
|
|
}{tt, cards})
|
|
}
|
|
}
|
|
for tt, cards := range tasks {
|
|
if tt != PlanTask && tt != MainTask && tt != MemoryCompressionTask {
|
|
result = append(result, struct {
|
|
Type TaskType
|
|
Cards []*TaskCard
|
|
}{tt, cards})
|
|
}
|
|
}
|
|
return result
|
|
},
|
|
}
|
|
content, err := assets.ReadFile("templates/" + name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return template.New(name).Funcs(funcMap).Parse(string(content))
|
|
}
|
|
|
|
func truncateText(n int, s string) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return s[:n] + "…"
|
|
}
|
|
|
|
func renderTemplate(w http.ResponseWriter, name string, data any) {
|
|
tmpl, err := parseTemplate(name)
|
|
if err != nil {
|
|
http.Error(w, "template error: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
if err := tmpl.Execute(w, data); err != nil {
|
|
// Partially written — just log
|
|
fmt.Printf("[viewer] template execution error: %v\n", err)
|
|
}
|
|
}
|
|
|
|
func staticFS() fs.FS {
|
|
sub, err := fs.Sub(assets, "static")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return sub
|
|
}
|
|
|
|
func formatDuration(seconds float64) string {
|
|
d := time.Duration(seconds * float64(time.Second))
|
|
if d < time.Minute {
|
|
return fmt.Sprintf("%.1fs", seconds)
|
|
}
|
|
minutes := int(d.Minutes())
|
|
sec := int(d.Seconds()) - minutes*60
|
|
return fmt.Sprintf("%dm%ds", minutes, sec)
|
|
}
|