package web import ( "fmt" "net/http" "strconv" "strings" "github.com/gin-gonic/gin" cairnapi "github.com/mattnite/cairn/internal/api" "github.com/mattnite/cairn/internal/models" "github.com/mattnite/cairn/internal/regression" "gorm.io/gorm" ) type PageHandler struct { DB *gorm.DB Templates *Templates ForgejoURL string } type PageData struct { Title string Content any } func (h *PageHandler) Index(c *gin.Context) { ctx := c.Request.Context() artifacts, total, err := models.ListArtifacts(ctx, h.DB, models.ListArtifactsParams{Limit: 10}) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } repos, err := models.ListRepositories(ctx, h.DB) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } var totalCG, openCG int64 _ = h.DB.WithContext(ctx).Model(&models.CrashGroup{}).Count(&totalCG).Error _ = h.DB.WithContext(ctx).Model(&models.CrashGroup{}).Where("status = ?", "open").Count(&openCG).Error // Top crashers. type topCrasher struct { CrashGroupID uint Title string OccurrenceCount uint RepoName string } var topCrashers []topCrasher groups, _, err := models.ListCrashGroups(ctx, h.DB, nil, "open", 5, 0) if err == nil { for _, group := range groups { topCrashers = append(topCrashers, topCrasher{ CrashGroupID: group.ID, Title: group.Title, OccurrenceCount: group.OccurrenceCount, RepoName: group.RepoName, }) } } data := PageData{ Title: "Dashboard", Content: map[string]any{ "Artifacts": artifacts, "TotalArtifacts": int(total), "Repositories": repos, "TotalCrashGroups": int(totalCG), "OpenCrashGroups": int(openCG), "TopCrashers": topCrashers, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "index", data) } func (h *PageHandler) Artifacts(c *gin.Context) { limit, _ := strconv.Atoi(c.Query("limit")) offset, _ := strconv.Atoi(c.Query("offset")) if limit <= 0 { limit = 50 } repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } artifacts, total, err := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{ RepositoryID: repoID, Type: c.Query("type"), Limit: limit, Offset: offset, }) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } data := PageData{ Title: "Artifacts", Content: map[string]any{ "Artifacts": artifacts, "Total": int(total), "Limit": limit, "Offset": offset, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "artifacts", data) } func (h *PageHandler) ArtifactDetail(c *gin.Context) { id, err := parseUintID(c.Param("id"), "artifact id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } artifact, err := models.GetArtifact(c.Request.Context(), h.DB, id) if err != nil { c.String(http.StatusNotFound, "artifact not found") return } data := PageData{ Title: fmt.Sprintf("Artifact %d", artifact.ID), Content: artifact, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "artifact_detail", data) } func (h *PageHandler) Repos(c *gin.Context) { repos, err := models.ListRepositories(c.Request.Context(), h.DB) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } data := PageData{ Title: "Repositories", Content: map[string]any{ "Repositories": repos, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "repos", data) } func (h *PageHandler) CrashGroups(c *gin.Context) { limit, _ := strconv.Atoi(c.Query("limit")) offset, _ := strconv.Atoi(c.Query("offset")) if limit <= 0 { limit = 50 } repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } groups, total, err := models.ListCrashGroups(c.Request.Context(), h.DB, repoID, c.Query("status"), limit, offset) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } data := PageData{ Title: "Crash Groups", Content: map[string]any{ "CrashGroups": groups, "Total": int(total), "Limit": limit, "Offset": offset, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "crashgroups", data) } func (h *PageHandler) CrashGroupDetail(c *gin.Context) { id, err := parseUintID(c.Param("id"), "crash group id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } group, err := models.GetCrashGroup(c.Request.Context(), h.DB, id) if err != nil { c.String(http.StatusNotFound, "crash group not found") return } if group.ForgejoIssueID != nil && h.ForgejoURL != "" { repo, repoErr := models.GetRepositoryByID(c.Request.Context(), h.DB, group.RepositoryID) if repoErr == nil { issueURL := fmt.Sprintf("%s/%s/%s/issues/%d", strings.TrimRight(h.ForgejoURL, "/"), repo.Owner, repo.Name, *group.ForgejoIssueID) group.ForgejoIssueURL = &issueURL } } // Get artifacts linked to this crash group's signature. signatureID := group.CrashSignatureID artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{ SignatureID: &signatureID, Limit: 50, }) data := PageData{ Title: "Crash Group: " + group.Title, Content: map[string]any{ "Group": group, "Artifacts": artifacts, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "crashgroup_detail", data) } func (h *PageHandler) Search(c *gin.Context) { q := c.Query("q") var artifacts []cairnapi.Artifact var total int64 if q != "" { artifacts, total, _ = models.SearchArtifacts(c.Request.Context(), h.DB, q, 50, 0) } data := PageData{ Title: "Search", Content: map[string]any{ "Query": q, "Artifacts": artifacts, "Total": int(total), }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "search", data) } func (h *PageHandler) Regression(c *gin.Context) { repo := c.Query("repo") base := c.Query("base") head := c.Query("head") content := map[string]any{ "Repo": repo, "Base": base, "Head": head, } if repo != "" && base != "" && head != "" { r, err := models.GetRepositoryByName(c.Request.Context(), h.DB, repo) if err == nil { result, err := regression.Compare(c.Request.Context(), h.DB, r.ID, base, head) if err == nil { result.RepoName = repo content["Result"] = result } } } data := PageData{ Title: "Regression Check", Content: content, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "regression", data) } func (h *PageHandler) Targets(c *gin.Context) { limit, _ := strconv.Atoi(c.Query("limit")) offset, _ := strconv.Atoi(c.Query("offset")) if limit <= 0 { limit = 50 } repoID, err := parseOptionalUintID(c.Query("repository_id"), "repository_id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } targets, total, err := models.ListTargets(c.Request.Context(), h.DB, repoID, limit, offset) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } data := PageData{ Title: "Targets", Content: map[string]any{ "Targets": targets, "Total": int(total), }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "targets", data) } func (h *PageHandler) TargetDetail(c *gin.Context) { id, err := parseUintID(c.Param("id"), "target id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } target, err := models.GetTarget(c.Request.Context(), h.DB, id) if err != nil { c.String(http.StatusNotFound, "target not found") return } targetID := target.ID runs, _, _ := models.ListRuns(c.Request.Context(), h.DB, &targetID, 50, 0) corpus, corpusTotal, _ := models.ListCorpusEntries(c.Request.Context(), h.DB, targetID, 50, 0) data := PageData{ Title: "Target: " + target.Name, Content: map[string]any{ "Target": target, "Runs": runs, "Corpus": corpus, "CorpusTotal": corpusTotal, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "target_detail", data) } func (h *PageHandler) RunDetail(c *gin.Context) { id, err := parseUintID(c.Param("id"), "run id") if err != nil { c.String(http.StatusBadRequest, err.Error()) return } run, err := models.GetRun(c.Request.Context(), h.DB, id) if err != nil { c.String(http.StatusNotFound, "run not found") return } runID := run.ID artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.DB, models.ListArtifactsParams{ RunID: &runID, Limit: 50, }) data := PageData{ Title: fmt.Sprintf("Run #%d", run.ID), Content: map[string]any{ "Run": run, "Artifacts": artifacts, }, } c.Header("Content-Type", "text/html; charset=utf-8") _ = h.Templates.Render(c.Writer, "run_detail", data) } func parseUintID(raw string, field string) (uint, error) { id, err := strconv.ParseUint(raw, 10, 64) if err != nil { return 0, fmt.Errorf("invalid %s", field) } return uint(id), nil } func parseOptionalUintID(raw string, field string) (*uint, error) { if raw == "" { return nil, nil } id, err := parseUintID(raw, field) if err != nil { return nil, err } return &id, nil }