package web import ( "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/jackc/pgx/v5/pgxpool" "github.com/mattnite/cairn/internal/models" "github.com/mattnite/cairn/internal/regression" ) type PageHandler struct { Pool *pgxpool.Pool Templates *Templates } 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.Pool, models.ListArtifactsParams{ Limit: 10, }) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } repos, err := models.ListRepositories(ctx, h.Pool) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } var totalCG, openCG int h.Pool.QueryRow(ctx, "SELECT COUNT(*) FROM crash_groups").Scan(&totalCG) h.Pool.QueryRow(ctx, "SELECT COUNT(*) FROM crash_groups WHERE status = 'open'").Scan(&openCG) // Top crashers type topCrasher struct { CrashGroupID string Title string OccurrenceCount int RepoName string } var topCrashers []topCrasher rows, err := h.Pool.Query(ctx, ` SELECT cg.id, cg.title, cs.occurrence_count, r.name FROM crash_groups cg JOIN crash_signatures cs ON cs.id = cg.crash_signature_id JOIN repositories r ON r.id = cg.repository_id WHERE cg.status = 'open' ORDER BY cs.occurrence_count DESC LIMIT 5 `) if err == nil { defer rows.Close() for rows.Next() { var tc topCrasher if rows.Scan(&tc.CrashGroupID, &tc.Title, &tc.OccurrenceCount, &tc.RepoName) == nil { topCrashers = append(topCrashers, tc) } } } data := PageData{ Title: "Dashboard", Content: map[string]any{ "Artifacts": artifacts, "TotalArtifacts": total, "Repositories": repos, "TotalCrashGroups": totalCG, "OpenCrashGroups": 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 } artifacts, total, err := models.ListArtifacts(c.Request.Context(), h.Pool, models.ListArtifactsParams{ RepositoryID: c.Query("repository_id"), 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": 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 := c.Param("id") artifact, err := models.GetArtifact(c.Request.Context(), h.Pool, id) if err != nil { c.String(http.StatusNotFound, "artifact not found") return } data := PageData{ Title: "Artifact " + artifact.ID[:8], 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.Pool) 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 } groups, total, err := models.ListCrashGroups( c.Request.Context(), h.Pool, c.Query("repository_id"), 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": 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 := c.Param("id") group, err := models.GetCrashGroup(c.Request.Context(), h.Pool, id) if err != nil { c.String(http.StatusNotFound, "crash group not found") return } // Get artifacts linked to this crash group's signature. artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.Pool, models.ListArtifactsParams{ SignatureID: group.CrashSignatureID, 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 []models.Artifact var total int if q != "" { artifacts, total, _ = models.SearchArtifacts(c.Request.Context(), h.Pool, q, 50, 0) } data := PageData{ Title: "Search", Content: map[string]any{ "Query": q, "Artifacts": artifacts, "Total": 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.Pool, repo) if err == nil { result, err := regression.Compare(c.Request.Context(), h.Pool, 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) Campaigns(c *gin.Context) { limit, _ := strconv.Atoi(c.Query("limit")) offset, _ := strconv.Atoi(c.Query("offset")) if limit <= 0 { limit = 50 } campaigns, total, err := models.ListCampaigns(c.Request.Context(), h.Pool, c.Query("repository_id"), limit, offset) if err != nil { c.String(http.StatusInternalServerError, err.Error()) return } data := PageData{ Title: "Campaigns", Content: map[string]any{ "Campaigns": campaigns, "Total": total, }, } c.Header("Content-Type", "text/html; charset=utf-8") h.Templates.Render(c.Writer, "campaigns", data) } func (h *PageHandler) CampaignDetail(c *gin.Context) { id := c.Param("id") campaign, err := models.GetCampaign(c.Request.Context(), h.Pool, id) if err != nil { c.String(http.StatusNotFound, "campaign not found") return } artifacts, _, _ := models.ListArtifacts(c.Request.Context(), h.Pool, models.ListArtifactsParams{ CampaignID: campaign.ID, Limit: 50, }) data := PageData{ Title: "Campaign: " + campaign.Name, Content: map[string]any{ "Campaign": campaign, "Artifacts": artifacts, }, } c.Header("Content-Type", "text/html; charset=utf-8") h.Templates.Render(c.Writer, "campaign_detail", data) }