package handlers import ( "net/http" "xorm.io/xorm" "github.com/forgeo/forgebucket/internal/api/middleware" "github.com/forgeo/forgebucket/internal/models" ) type DashboardHandler struct{ db *xorm.Engine } func NewDashboardHandler(db *xorm.Engine) *DashboardHandler { return &DashboardHandler{db: db} } // ── Response shapes ─────────────────────────────────────────────────────────── type dashStats struct { RepoCount int `json:"repoCount"` OpenPRs int `json:"openPRs"` ReviewQueue int `json:"reviewQueue"` OpenIssues int `json:"openIssues"` } type dashPR struct { ID int64 `json:"id"` Title string `json:"title"` SourceBranch string `json:"sourceBranch"` TargetBranch string `json:"targetBranch"` Status string `json:"status"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` RepoID int64 `json:"repoId"` RepoName string `json:"repoName"` OwnerName string `json:"ownerName"` AuthorID int64 `json:"authorId"` } type dashIssue struct { ID int64 `json:"id"` Number int `json:"number"` Title string `json:"title"` State string `json:"state"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` RepoID int64 `json:"repoId"` RepoName string `json:"repoName"` OwnerName string `json:"ownerName"` } type dashRepo struct { ID int64 `json:"id"` Name string `json:"name"` Description string `json:"description"` IsPrivate bool `json:"isPrivate"` DefaultBranch string `json:"defaultBranch"` UpdatedAt string `json:"updatedAt"` OwnerName string `json:"ownerName"` AvatarURL string `json:"avatarUrl"` OpenPRCount int `json:"openPrCount"` OpenIssueCount int `json:"openIssueCount"` } type dashRun struct { ID int64 `json:"id"` RepoID int64 `json:"repoId"` RepoName string `json:"repoName"` OwnerName string `json:"ownerName"` TriggerRef string `json:"triggerRef"` TriggerSHA string `json:"triggerSha"` TriggeredBy string `json:"triggeredBy"` Status string `json:"status"` StartedAt *string `json:"startedAt"` FinishedAt *string `json:"finishedAt"` CreatedAt string `json:"createdAt"` } type dashboardResponse struct { Stats dashStats `json:"stats"` ReviewQueue []dashPR `json:"reviewQueue"` MyOpenPRs []dashPR `json:"myOpenPRs"` MyOpenIssues []dashIssue `json:"myOpenIssues"` Repos []dashRepo `json:"repos"` RecentRuns []dashRun `json:"recentRuns"` } // ── Handler ─────────────────────────────────────────────────────────────────── func (h *DashboardHandler) Get(w http.ResponseWriter, r *http.Request) { userID, _ := middleware.UserIDFromContext(r.Context()) // 1. Repos owned by this user. var repos []models.Repository h.db.Where("owner_id = ?", userID).Desc("updated_at").Find(&repos) repoIDs := make([]int64, len(repos)) repoByID := make(map[int64]models.Repository, len(repos)) for i, rp := range repos { repoIDs[i] = rp.ID repoByID[rp.ID] = rp } // Owner username — needed for URLs. var owner models.User h.db.ID(userID).Cols("id", "username", "avatar_url").Get(&owner) // 2. All open PRs across user repos. var allOpenPRs []models.PullRequest if len(repoIDs) > 0 { h.db.In("repo_id", repoIDs).Where("status = 'open'").Desc("updated_at").Find(&allOpenPRs) } // 3. PRs where user is assigned as reviewer (and PR is open). var reviewerRows []models.PrReviewer h.db.Where("user_id = ?", userID).Find(&reviewerRows) reviewPRIDs := make([]int64, 0, len(reviewerRows)) for _, rv := range reviewerRows { reviewPRIDs = append(reviewPRIDs, rv.PRID) } var reviewPRs []models.PullRequest if len(reviewPRIDs) > 0 { h.db.In("id", reviewPRIDs).Where("status = 'open' AND author_id != ?", userID). Desc("updated_at").Find(&reviewPRs) } // 4. Open issues authored by user across their repos. var openIssues []models.Issue if len(repoIDs) > 0 { h.db.In("repo_id", repoIDs).Where("author_id = ? AND state = 'open'", userID). Desc("updated_at").Limit(20).Find(&openIssues) } // 5. Build per-repo counters. prCountByRepo := make(map[int64]int) for _, pr := range allOpenPRs { prCountByRepo[pr.RepoID]++ } issueCountByRepo := make(map[int64]int) for _, iss := range openIssues { issueCountByRepo[iss.RepoID]++ } // 6. Separate my PRs from the full open list. var myOpenPRs []models.PullRequest for _, pr := range allOpenPRs { if pr.AuthorID == userID { myOpenPRs = append(myOpenPRs, pr) } } // ── Build response ───────────────────────────────────────────────────────── toDashPR := func(pr models.PullRequest) dashPR { rp := repoByID[pr.RepoID] return dashPR{ ID: pr.ID, Title: pr.Title, SourceBranch: pr.SourceBranch, TargetBranch: pr.TargetBranch, Status: string(pr.Status), CreatedAt: pr.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: pr.UpdatedAt.Format("2006-01-02T15:04:05Z"), RepoID: pr.RepoID, RepoName: rp.Name, OwnerName: owner.Username, AuthorID: pr.AuthorID, } } // For review queue PRs that may be on OTHER users' repos, look up owner. toReviewPR := func(pr models.PullRequest) dashPR { rp := repoByID[pr.RepoID] dp := toDashPR(pr) if rp.ID == 0 { // PR is on a repo the user doesn't own (they're just a member). var foreignRepo models.Repository if found, _ := h.db.ID(pr.RepoID).Get(&foreignRepo); found { var foreignOwner models.User h.db.ID(foreignRepo.OwnerID).Cols("username").Get(&foreignOwner) dp.RepoName = foreignRepo.Name dp.OwnerName = foreignOwner.Username } } return dp } dashRepos := make([]dashRepo, 0, len(repos)) for _, rp := range repos { dashRepos = append(dashRepos, dashRepo{ ID: rp.ID, Name: rp.Name, Description: rp.Description, IsPrivate: rp.IsPrivate, DefaultBranch: rp.DefaultBranch, UpdatedAt: rp.UpdatedAt.Format("2006-01-02T15:04:05Z"), OwnerName: owner.Username, AvatarURL: "/api/v1/repos/" + owner.Username + "/" + rp.Name + "/avatar", OpenPRCount: prCountByRepo[rp.ID], OpenIssueCount: issueCountByRepo[rp.ID], }) } myPRDash := make([]dashPR, 0, len(myOpenPRs)) for _, pr := range myOpenPRs { myPRDash = append(myPRDash, toDashPR(pr)) } reviewQueue := make([]dashPR, 0, len(reviewPRs)) for _, pr := range reviewPRs { reviewQueue = append(reviewQueue, toReviewPR(pr)) } issueDash := make([]dashIssue, 0, len(openIssues)) for _, iss := range openIssues { rp := repoByID[iss.RepoID] issueDash = append(issueDash, dashIssue{ ID: iss.ID, Number: iss.Number, Title: iss.Title, State: string(iss.State), CreatedAt: iss.CreatedAt.Format("2006-01-02T15:04:05Z"), UpdatedAt: iss.UpdatedAt.Format("2006-01-02T15:04:05Z"), RepoID: iss.RepoID, RepoName: rp.Name, OwnerName: owner.Username, }) } // 7. Recent CI runs across user repos. var recentRuns []models.PipelineRun if len(repoIDs) > 0 { h.db.In("repo_id", repoIDs).Desc("id").Limit(5).Find(&recentRuns) } runsDash := make([]dashRun, 0, len(recentRuns)) for _, run := range recentRuns { rp := repoByID[run.RepoID] dr := dashRun{ ID: run.ID, RepoID: run.RepoID, RepoName: rp.Name, OwnerName: owner.Username, TriggerRef: run.TriggerRef, TriggerSHA: run.TriggerSHA, TriggeredBy: run.TriggeredBy, Status: run.Status, CreatedAt: run.CreatedAt.Format("2006-01-02T15:04:05Z"), } if run.StartedAt != nil { s := run.StartedAt.Format("2006-01-02T15:04:05Z") dr.StartedAt = &s } if run.FinishedAt != nil { f := run.FinishedAt.Format("2006-01-02T15:04:05Z") dr.FinishedAt = &f } runsDash = append(runsDash, dr) } resp := dashboardResponse{ Stats: dashStats{ RepoCount: len(repos), OpenPRs: len(myOpenPRs), ReviewQueue: len(reviewPRs), OpenIssues: len(openIssues), }, ReviewQueue: reviewQueue, MyOpenPRs: myPRDash, MyOpenIssues: issueDash, Repos: dashRepos, RecentRuns: runsDash, } jsonOK(w, resp) }