229 lines
6.4 KiB
Go
229 lines
6.4 KiB
Go
package logweb
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"io/fs"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"xk-hy-transit-go/internal/applog"
|
||
"xk-hy-transit-go/internal/config"
|
||
"xk-hy-transit-go/internal/db"
|
||
"xk-hy-transit-go/internal/hy"
|
||
syncer "xk-hy-transit-go/internal/sync"
|
||
"xk-hy-transit-go/internal/syncgate"
|
||
)
|
||
|
||
// Runner 同步执行器(与 serve 共用)。
|
||
type Runner interface {
|
||
Run(step, anchorDate string) error
|
||
}
|
||
|
||
// Deps Web 控制台依赖。
|
||
type Deps struct {
|
||
LogDir string
|
||
Store *db.Store
|
||
Runner Runner
|
||
Cfg config.Config
|
||
AllowTest bool
|
||
MySQLOK bool
|
||
MySQLErr string
|
||
}
|
||
|
||
// Server HTTP 控制台。
|
||
type Server struct {
|
||
deps Deps
|
||
mux *http.ServeMux
|
||
}
|
||
|
||
// NewServer 创建服务。
|
||
func NewServer(deps Deps) *Server {
|
||
if deps.LogDir == "" {
|
||
deps.LogDir = applog.Dir()
|
||
}
|
||
s := &Server{deps: deps, mux: http.NewServeMux()}
|
||
s.routes()
|
||
return s
|
||
}
|
||
|
||
func (s *Server) routes() {
|
||
sub, _ := fs.Sub(webFS, "web")
|
||
fileServer := http.FileServer(http.FS(sub))
|
||
|
||
s.mux.HandleFunc("/", s.handleIndex)
|
||
s.mux.HandleFunc("/logs", s.handlePage("logs.html"))
|
||
s.mux.HandleFunc("/logs/view", s.handlePage("logs_view.html"))
|
||
s.mux.HandleFunc("/runs", s.handlePage("runs.html"))
|
||
s.mux.HandleFunc("/runs/record", s.handlePage("runs_record.html"))
|
||
s.mux.HandleFunc("/test", s.handlePage("test.html"))
|
||
|
||
s.mux.HandleFunc("/api/meta", s.handleAPIMeta)
|
||
s.mux.HandleFunc("/api/dates", s.handleAPIDates)
|
||
s.mux.HandleFunc("/api/log", s.handleAPILog)
|
||
s.mux.HandleFunc("/api/runs", s.handleAPIRuns)
|
||
s.mux.HandleFunc("/api/runs/record", s.handleAPIRecord)
|
||
s.mux.HandleFunc("/api/test/status", s.handleAPITestStatus)
|
||
s.mux.HandleFunc("/api/test/sync", s.handleAPITestSync)
|
||
|
||
s.mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
|
||
}
|
||
|
||
func (s *Server) handlePage(name string) http.HandlerFunc {
|
||
return func(w http.ResponseWriter, r *http.Request) {
|
||
b, err := webFS.ReadFile("web/" + name)
|
||
if err != nil {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||
_, _ = w.Write(b)
|
||
}
|
||
}
|
||
|
||
func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
|
||
if r.URL.Path != "/" {
|
||
http.NotFound(w, r)
|
||
return
|
||
}
|
||
s.handlePage("index.html")(w, r)
|
||
}
|
||
|
||
func (s *Server) handleAPIMeta(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(w, map[string]any{
|
||
"logDir": s.deps.LogDir,
|
||
"mysqlOk": s.deps.MySQLOK,
|
||
"mysqlError": s.deps.MySQLErr,
|
||
"allowTest": s.deps.AllowTest && s.deps.Runner != nil,
|
||
"hasRunner": s.deps.Runner != nil,
|
||
"stepOptions": hy.StepSelectOptions(),
|
||
"testStepOptions": hy.TestStepSelectOptions(),
|
||
"logKindOptions": []map[string]string{{"value": "app", "label": "应用"}, {"value": "pull", "label": "拉取"}, {"value": "push", "label": "推送"}},
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleAPIDates(w http.ResponseWriter, r *http.Request) {
|
||
kind := r.URL.Query().Get("kind")
|
||
dates, err := ListLogDates(s.deps.LogDir, kind)
|
||
if err != nil {
|
||
writeErr(w, 400, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, dates)
|
||
}
|
||
|
||
func (s *Server) handleAPILog(w http.ResponseWriter, r *http.Request) {
|
||
kind := r.URL.Query().Get("kind")
|
||
date := r.URL.Query().Get("date")
|
||
meta, lines, err := ReadLogFile(s.deps.LogDir, kind, date)
|
||
if err != nil {
|
||
writeErr(w, 404, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, map[string]any{
|
||
"meta": meta,
|
||
"lines": ParseLogLines(lines),
|
||
})
|
||
}
|
||
|
||
func (s *Server) handleAPIRuns(w http.ResponseWriter, r *http.Request) {
|
||
if s.deps.Store == nil {
|
||
writeErr(w, 503, "MySQL 未连接")
|
||
return
|
||
}
|
||
limit, _ := strconv.Atoi(r.URL.Query().Get("limit"))
|
||
items, err := ListRuns(s.deps.Store, r.URL.Query().Get("anchor_date"), r.URL.Query().Get("step"), limit)
|
||
if err != nil {
|
||
writeErr(w, 500, err.Error())
|
||
return
|
||
}
|
||
jobs, _ := s.deps.Store.ListJobs(30, r.URL.Query().Get("anchor_date"), r.URL.Query().Get("step"))
|
||
writeJSON(w, map[string]any{"items": items, "jobs": enrichJobs(jobs)})
|
||
}
|
||
|
||
func (s *Server) handleAPIRecord(w http.ResponseWriter, r *http.Request) {
|
||
if s.deps.Store == nil {
|
||
writeErr(w, 503, "MySQL 未连接")
|
||
return
|
||
}
|
||
id, _ := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
|
||
if id <= 0 {
|
||
writeErr(w, 400, "invalid id")
|
||
return
|
||
}
|
||
detail, err := GetRecordDetail(s.deps.Store, id)
|
||
if err != nil {
|
||
writeErr(w, 404, err.Error())
|
||
return
|
||
}
|
||
writeJSON(w, detail)
|
||
}
|
||
|
||
func (s *Server) handleAPITestStatus(w http.ResponseWriter, r *http.Request) {
|
||
writeJSON(w, enrichTestStatus(syncgate.GetStatus()))
|
||
}
|
||
|
||
func (s *Server) handleAPITestSync(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != http.MethodPost {
|
||
writeErr(w, 405, "POST only")
|
||
return
|
||
}
|
||
if !s.deps.AllowTest || s.deps.Runner == nil {
|
||
writeErr(w, 403, "测试同步未启用或未配置 Runner(请使用 transit serve)")
|
||
return
|
||
}
|
||
var body struct {
|
||
Step string `json:"step"`
|
||
Date string `json:"date"`
|
||
}
|
||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||
writeErr(w, 400, "invalid json")
|
||
return
|
||
}
|
||
step := strings.TrimSpace(body.Step)
|
||
if step == "" {
|
||
step = "all"
|
||
}
|
||
anchor := syncer.ResolveAnchorDate(s.deps.Cfg, strings.TrimSpace(body.Date))
|
||
runner := s.deps.Runner
|
||
if !syncgate.TryRun(step, anchor, func() error {
|
||
applog.RunSeparator(fmt.Sprintf("BEGIN web-test step=%s date=%s", step, anchor))
|
||
applog.Appf("web test sync start step=%s date=%s", step, anchor)
|
||
err := runner.Run(step, anchor)
|
||
if err != nil {
|
||
applog.Appf("web test sync failed step=%s date=%s err=%v", step, anchor, err)
|
||
applog.RunSeparator(fmt.Sprintf("END web-test step=%s date=%s ok=false", step, anchor))
|
||
return err
|
||
}
|
||
applog.Appf("web test sync done step=%s date=%s", step, anchor)
|
||
applog.RunSeparator(fmt.Sprintf("END web-test step=%s date=%s ok=true", step, anchor))
|
||
return nil
|
||
}) {
|
||
writeErr(w, 409, "已有同步任务在执行中")
|
||
return
|
||
}
|
||
writeJSON(w, map[string]any{"ok": true, "step": step, "date": anchor})
|
||
}
|
||
|
||
// Run 启动 HTTP 服务(阻塞)。
|
||
func Run(addr string, deps Deps) error {
|
||
s := NewServer(deps)
|
||
log.Printf("logweb: 控制台 http://%s/", addr)
|
||
return http.ListenAndServe(addr, s.mux)
|
||
}
|
||
|
||
func writeJSON(w http.ResponseWriter, v any) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
enc := json.NewEncoder(w)
|
||
enc.SetIndent("", " ")
|
||
_ = enc.Encode(v)
|
||
}
|
||
|
||
func writeErr(w http.ResponseWriter, code int, msg string) {
|
||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||
w.WriteHeader(code)
|
||
_ = json.NewEncoder(w).Encode(map[string]string{"error": msg})
|
||
}
|