Files
xk-hy-transit-go/internal/logweb/server.go
2026-05-22 09:17:39 +08:00

229 lines
6.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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})
}