package main import ( "bytes" "encoding/json" "errors" "html/template" "io" "net/http" "os" "path/filepath" "sort" "strconv" "strings" ) func ucNumber(id string) int { n, _ := strconv.Atoi(strings.TrimPrefix(id, "W-")); return n } type MockupView struct { ModuleNames map[string]string FlowGroups []UCModuleGroup Tab string Screens []MockScreen Selected *MockScreen Catalog MockCatalog UseCases []UseCase UseCase *UseCase Query, Module, Channel, Status string Modules, Channels []string Total int Key string Components []struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } } type mockGraphResponse struct { Graph MockGraph `json:"graph"` CurrentSourceHash string `json:"current_source_hash"` SourceChanged bool `json:"source_changed"` } func mockDecode(w http.ResponseWriter, r *http.Request, v any) error { r.Body = http.MaxBytesReader(w, r.Body, 4<<20) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if err := d.Decode(v); err != nil { return err } var extra any if d.Decode(&extra) != io.EOF { return errors.New("one JSON value required") } return nil } func mockJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.Header().Set("Cache-Control", "no-store") json.NewEncoder(w).Encode(v) } func registerMockups(mux *http.ServeMux, store *Store) { tpl := template.Must(template.New("").Funcs(templateFuncs()).ParseFS(templateFS, "templates/layout.html", "templates/partials.html", "templates/mockups.html")) ms := &mockStore{dir: store.dir} page := func(w http.ResponseWriter, r *http.Request) { c, err := loadMockCatalog(repoRoot) if err != nil { http.Error(w, "อ่าน artifacts ไม่สำเร็จ", 500) return } uc, err := loadUseCases(repoRoot) if err != nil { http.Error(w, "อ่าน Use cases ไม่สำเร็จ", 500) return } v := MockupView{Tab: "sitemap", Catalog: c, Total: len(c.Screens), Query: strings.TrimSpace(r.URL.Query().Get("q")), Module: r.URL.Query().Get("module"), Channel: r.URL.Query().Get("channel"), Status: r.URL.Query().Get("status"), Key: "sitemap"} v.ModuleNames = c.ModuleNames if strings.HasPrefix(r.URL.Path, "/mockup/screens") { v.Tab = "screens" } if strings.HasPrefix(r.URL.Path, "/mockup/page-flow") { v.Tab = "flow" v.Key = "" } modules, channels := map[string]bool{}, map[string]bool{} for _, s := range c.Screens { for _, m := range s.Modules { modules[m] = true } channels[s.Channel] = true search := strings.ToLower(s.ID + " " + s.Title + " " + strings.Join(s.UseCases, " ")) if v.Query != "" && !strings.Contains(search, strings.ToLower(v.Query)) { continue } if v.Module != "" && !containsUC(s.Modules, v.Module) { continue } if v.Channel != "" && s.Channel != v.Channel { continue } if v.Status != "" && s.Status != v.Status { continue } v.Screens = append(v.Screens, s) } for m := range modules { v.Modules = append(v.Modules, m) } for ch := range channels { v.Channels = append(v.Channels, ch) } sort.Strings(v.Modules) sort.Strings(v.Channels) for _, u := range uc.UC { v.UseCases = append(v.UseCases, u) } sort.Slice(v.UseCases, func(i, j int) bool { return ucNumber(v.UseCases[i].ID) < ucNumber(v.UseCases[j].ID) }) codes := []string{} for code := range uc.Modules { codes = append(codes, code) } sort.Strings(codes) for _, code := range codes { group := UCModuleGroup{Code: code, Name: moduleLabel(v.ModuleNames, code), Group: uc.Modules[code].Group} for _, u := range v.UseCases { if len(u.Modules) > 0 && u.Modules[0] == code { group.Items = append(group.Items, u) } } if len(group.Items) > 0 { v.FlowGroups = append(v.FlowGroups, group) } } if sid := r.PathValue("sid"); sid != "" { s, ok := c.ByID[sid] if !ok { http.NotFound(w, r) return } v.Selected = &s } if id := r.PathValue("uid"); id != "" { u, ok := uc.UC[id] if !ok { http.NotFound(w, r) return } v.UseCase = &u v.Key = id } if b, err := mockFile(repoRoot, "mockups/design-system/components.json"); err == nil { var defs struct { Components []struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description"` } `json:"components"` } if json.Unmarshal(b, &defs) == nil { v.Components = defs.Components } } p := buildPage(store, "mockup", "Mockup") p.Mockups = &v var b bytes.Buffer if err = tpl.ExecuteTemplate(&b, "layout", p); err != nil { http.Error(w, "render mockup error: "+err.Error(), 500) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Header().Set("Cache-Control", "no-store") b.WriteTo(w) } for _, path := range []string{"/mockup", "/mockup/sitemap", "/mockup/page-flow", "/mockup/page-flow/{uid}", "/mockup/screens", "/mockup/screens/{sid}"} { mux.HandleFunc("GET "+path, page) } mux.HandleFunc("GET /api/mockups/catalog", func(w http.ResponseWriter, r *http.Request) { c, err := loadMockCatalog(repoRoot) if err != nil { http.Error(w, "อ่าน catalog ไม่สำเร็จ", 500) return } mockJSON(w, c) }) mux.HandleFunc("GET /mockup-assets/{path...}", func(w http.ResponseWriter, r *http.Request) { c, err := loadMockCatalog(repoRoot) path := r.PathValue("path") if err != nil || !c.Assets[path] { http.NotFound(w, r) return } b, err := mockFile(repoRoot, path) if err != nil { http.NotFound(w, r) return } w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; sandbox") w.Header().Set("X-Content-Type-Options", "nosniff") if filepath.Ext(path) == ".svg" { w.Header().Set("Content-Type", "image/svg+xml") } else { w.Header().Set("Content-Type", http.DetectContentType(b)) } w.Write(b) }) source := func(w http.ResponseWriter, r *http.Request) (string, MockCatalog, MockGraph, bool) { key := r.PathValue("key") c, err := loadMockCatalog(repoRoot) if err != nil { http.Error(w, "อ่าน catalog ไม่สำเร็จ", 500) return key, c, MockGraph{}, false } var draft MockGraph if key == "sitemap" { draft, err = draftMockSitemap(repoRoot, c) } else { uc, e := loadUseCases(repoRoot) u, ok := uc.UC[key] if e != nil || !ok { http.NotFound(w, r) return key, c, draft, false } draft, err = draftMockFlow(repoRoot, u, c) } if err != nil { http.Error(w, "อ่านต้นฉบับ graph ไม่สำเร็จ", 500) return key, c, draft, false } return key, c, draft, true } mux.HandleFunc("GET /api/mockups/graphs/{key}", func(w http.ResponseWriter, r *http.Request) { key, c, draft, ok := source(w, r) if !ok { return } g, err := ms.load(key, draft) if err != nil { http.Error(w, "อ่าน diagram ที่บันทึกไม่สำเร็จ ไม่มีการรีเซ็ตข้อมูล", 500) return } if r.URL.Query().Get("draft") == "1" { rev := g.Revision g = draft g.Revision = rev } if err = validateMockGraph(g, c); err != nil { http.Error(w, "diagram มี screen/variant ที่ไม่อยู่ใน catalog กรุณาตรวจ artifacts ก่อน", 422) return } mockJSON(w, mockGraphResponse{Graph: g, CurrentSourceHash: draft.SourceHash, SourceChanged: g.SourceHash != draft.SourceHash}) }) mux.HandleFunc("PUT /api/mockups/graphs/{key}", func(w http.ResponseWriter, r *http.Request) { if !sameOrigin(r) { http.Error(w, "ต้องบันทึกจาก tracker นี้", 403) return } key, c, draft, ok := source(w, r) if !ok { return } var g MockGraph if mockDecode(w, r, &g) != nil || g.Direction != draft.Direction { http.Error(w, "ข้อมูล graph ไม่ถูกต้อง", 400) return } saved, err := ms.save(key, g, c) if err != nil { code := 400 if errors.Is(err, errMockConflict) { code = 409 } http.Error(w, err.Error(), code) return } mockJSON(w, mockGraphResponse{Graph: saved, CurrentSourceHash: draft.SourceHash, SourceChanged: saved.SourceHash != draft.SourceHash}) }) mux.HandleFunc("GET /api/mockups/graphs/{key}/revisions", func(w http.ResponseWriter, r *http.Request) { key, _, _, ok := source(w, r) if !ok { return } history, err := ms.history(key) if err != nil { http.Error(w, "อ่านประวัติไม่สำเร็จ", 500) return } mockJSON(w, history) }) mux.HandleFunc("POST /api/mockups/graphs/{key}/restore", func(w http.ResponseWriter, r *http.Request) { if !sameOrigin(r) { http.Error(w, "ต้องคืน revision จาก tracker นี้", 403) return } key, c, draft, ok := source(w, r) if !ok { return } var sub struct { Revision int `json:"revision"` Target int `json:"target_revision"` } if mockDecode(w, r, &sub) != nil { http.Error(w, "ข้อมูล restore ไม่ถูกต้อง", 400) return } g, err := ms.restore(key, sub.Revision, sub.Target, c) if err != nil { code := 400 if errors.Is(err, errMockConflict) { code = 409 } if errors.Is(err, os.ErrNotExist) { code = 404 } http.Error(w, err.Error(), code) return } mockJSON(w, mockGraphResponse{Graph: g, CurrentSourceHash: draft.SourceHash, SourceChanged: g.SourceHash != draft.SourceHash}) }) }