diff --git a/mattermost-plugin-issues/server/api_context.go b/mattermost-plugin-issues/server/api_context.go index 9c003eadd86..4541bcf0ea0 100644 --- a/mattermost-plugin-issues/server/api_context.go +++ b/mattermost-plugin-issues/server/api_context.go @@ -31,25 +31,20 @@ type IssueContext struct { CycleName string `json:"cycle_name,omitempty"` } -// GET /api/v1/context/general -func (p *Plugin) handleGetGeneralContext(w http.ResponseWriter, _ *http.Request) { +func (p *Plugin) handleGetGeneralContext(w http.ResponseWriter, r *http.Request) { companyInfo, err := p.store.GetCompanyInfo() if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } - projects, err := p.store.ListProjects() + projects, err := p.listProjectsForRequest(r) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return } - result := GeneralContext{ - Company: companyInfo, - Projects: make([]ProjectContext, 0, len(projects)), - } - + result := GeneralContext{Company: companyInfo, Projects: make([]ProjectContext, 0, len(projects))} totalIssues := 0 openIssues := 0 @@ -77,15 +72,9 @@ func (p *Plugin) handleGetGeneralContext(w http.ResponseWriter, _ *http.Request) } } - result.Projects = append(result.Projects, ProjectContext{ - Project: project, - Issues: issues, - Labels: labels, - Cycles: cycles, - }) + result.Projects = append(result.Projects, ProjectContext{Project: project, Issues: issues, Labels: labels, Cycles: cycles}) } - // Fill in live stats if company info exists. if result.Company != nil { result.Company.State.ActiveProjects = len(projects) result.Company.State.TotalIssues = totalIssues @@ -95,11 +84,10 @@ func (p *Plugin) handleGetGeneralContext(w http.ResponseWriter, _ *http.Request) respondJSON(w, http.StatusOK, result) } -// GET /api/v1/projects/{id}/context func (p *Plugin) handleGetProjectContext(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - project, err := p.store.GetProject(id) + project, err := p.getProjectForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -121,25 +109,18 @@ func (p *Plugin) handleGetProjectContext(w http.ResponseWriter, r *http.Request) return } - respondJSON(w, http.StatusOK, ProjectContext{ - Project: project, - Issues: issues, - Labels: labels, - Cycles: cycles, - }) + respondJSON(w, http.StatusOK, ProjectContext{Project: project, Issues: issues, Labels: labels, Cycles: cycles}) } -// GET /api/v1/issues/{id}/context func (p *Plugin) handleGetIssueContext(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - issue, err := p.store.GetIssue(id) + issue, err := p.getIssueForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return } - // Resolve labels labels := make([]*IssueLabel, 0, len(issue.LabelIDs)) for _, labelID := range issue.LabelIDs { if label, err := p.store.GetLabel(labelID); err == nil { @@ -147,7 +128,6 @@ func (p *Plugin) handleGetIssueContext(w http.ResponseWriter, r *http.Request) { } } - // Resolve assignee name var assigneeName string if issue.AssigneeID != "" { if user, appErr := p.API.GetUser(issue.AssigneeID); appErr == nil { @@ -155,7 +135,6 @@ func (p *Plugin) handleGetIssueContext(w http.ResponseWriter, r *http.Request) { } } - // Resolve cycle name var cycleName string if issue.CycleID != "" { if cycle, err := p.store.GetCycle(issue.CycleID); err == nil { @@ -163,10 +142,5 @@ func (p *Plugin) handleGetIssueContext(w http.ResponseWriter, r *http.Request) { } } - respondJSON(w, http.StatusOK, IssueContext{ - Issue: issue, - Labels: labels, - AssigneeName: assigneeName, - CycleName: cycleName, - }) + respondJSON(w, http.StatusOK, IssueContext{Issue: issue, Labels: labels, AssigneeName: assigneeName, CycleName: cycleName}) } diff --git a/mattermost-plugin-issues/server/api_cycles.go b/mattermost-plugin-issues/server/api_cycles.go index 5354468b9ef..88ff65c5b4b 100644 --- a/mattermost-plugin-issues/server/api_cycles.go +++ b/mattermost-plugin-issues/server/api_cycles.go @@ -12,6 +12,10 @@ import ( func (p *Plugin) handleListCycles(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } cycles, err := p.store.ListCycles(projectID) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) @@ -22,6 +26,10 @@ func (p *Plugin) handleListCycles(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleCreateCycle(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } var req CreateCycleRequest if err := decodeJSON(r, &req); err != nil { @@ -29,15 +37,7 @@ func (p *Plugin) handleCreateCycle(w http.ResponseWriter, r *http.Request) { return } - cycle := &Cycle{ - ID: uuid.New().String(), - ProjectID: projectID, - Name: req.Name, - StartDate: req.StartDate, - EndDate: req.EndDate, - IsActive: false, - } - + cycle := &Cycle{ID: uuid.New().String(), ProjectID: projectID, Name: req.Name, StartDate: req.StartDate, EndDate: req.EndDate, IsActive: false} if err := cycle.IsValid(); err != nil { respondError(w, http.StatusBadRequest, err.Error()) return @@ -55,7 +55,7 @@ func (p *Plugin) handleCreateCycle(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleUpdateCycle(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - cycle, err := p.store.GetCycle(id) + cycle, err := p.getCycleForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -96,8 +96,7 @@ func (p *Plugin) handleUpdateCycle(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleDeleteCycle(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - - if _, err := p.store.GetCycle(id); err != nil { + if _, err := p.getCycleForRequest(r, id); err != nil { respondError(w, http.StatusNotFound, err.Error()) return } diff --git a/mattermost-plugin-issues/server/api_issues.go b/mattermost-plugin-issues/server/api_issues.go index d01524f1cd6..2f7033c5327 100644 --- a/mattermost-plugin-issues/server/api_issues.go +++ b/mattermost-plugin-issues/server/api_issues.go @@ -12,8 +12,12 @@ import ( func (p *Plugin) handleListIssues(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] - q := r.URL.Query() + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } + q := r.URL.Query() params := IssueFilterParams{ Status: q.Get("status"), Priority: q.Get("priority"), @@ -28,14 +32,15 @@ func (p *Plugin) handleListIssues(w http.ResponseWriter, r *http.Request) { return } - respondJSON(w, http.StatusOK, IssueListResponse{ - Issues: issues, - TotalCount: len(issues), - }) + respondJSON(w, http.StatusOK, IssueListResponse{Issues: issues, TotalCount: len(issues)}) } func (p *Plugin) handleCreateIssue(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } var req CreateIssueRequest if err := decodeJSON(r, &req); err != nil { @@ -90,7 +95,7 @@ func (p *Plugin) handleCreateIssue(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleGetIssue(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - issue, err := p.store.GetIssue(id) + issue, err := p.getIssueForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -101,7 +106,7 @@ func (p *Plugin) handleGetIssue(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleUpdateIssue(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - issue, err := p.store.GetIssue(id) + issue, err := p.getIssueForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -122,7 +127,6 @@ func (p *Plugin) handleUpdateIssue(w http.ResponseWriter, r *http.Request) { if req.Status != nil { oldStatus := issue.Status issue.Status = *req.Status - // Track completion time. if !oldStatus.IsCompleted() && issue.Status.IsCompleted() { issue.CompletedAt = nowMillis() } else if oldStatus.IsCompleted() && !issue.Status.IsCompleted() { @@ -167,13 +171,12 @@ func (p *Plugin) handleUpdateIssue(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleDeleteIssue(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - issue, err := p.store.GetIssue(id) + issue, err := p.getIssueForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return } - // Only creator or sysadmin can delete. userID := getUserID(r) if issue.CreatedBy != userID { user, appErr := p.API.GetUser(userID) diff --git a/mattermost-plugin-issues/server/api_labels.go b/mattermost-plugin-issues/server/api_labels.go index 83786708679..1799bc7f7b0 100644 --- a/mattermost-plugin-issues/server/api_labels.go +++ b/mattermost-plugin-issues/server/api_labels.go @@ -12,6 +12,10 @@ import ( func (p *Plugin) handleListLabels(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } labels, err := p.store.ListLabels(projectID) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) @@ -22,6 +26,10 @@ func (p *Plugin) handleListLabels(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleCreateLabel(w http.ResponseWriter, r *http.Request) { projectID := mux.Vars(r)["id"] + if _, err := p.getProjectForRequest(r, projectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } var req CreateLabelRequest if err := decodeJSON(r, &req); err != nil { @@ -29,13 +37,7 @@ func (p *Plugin) handleCreateLabel(w http.ResponseWriter, r *http.Request) { return } - label := &IssueLabel{ - ID: uuid.New().String(), - ProjectID: projectID, - Name: req.Name, - Color: req.Color, - } - + label := &IssueLabel{ID: uuid.New().String(), ProjectID: projectID, Name: req.Name, Color: req.Color} if err := label.IsValid(); err != nil { respondError(w, http.StatusBadRequest, err.Error()) return @@ -53,7 +55,7 @@ func (p *Plugin) handleCreateLabel(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleUpdateLabel(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - label, err := p.store.GetLabel(id) + label, err := p.getLabelForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -88,8 +90,7 @@ func (p *Plugin) handleUpdateLabel(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleDeleteLabel(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - - if _, err := p.store.GetLabel(id); err != nil { + if _, err := p.getLabelForRequest(r, id); err != nil { respondError(w, http.StatusNotFound, err.Error()) return } diff --git a/mattermost-plugin-issues/server/api_projects.go b/mattermost-plugin-issues/server/api_projects.go index 61c4edaa943..e545326162e 100644 --- a/mattermost-plugin-issues/server/api_projects.go +++ b/mattermost-plugin-issues/server/api_projects.go @@ -10,8 +10,8 @@ import ( "github.com/gorilla/mux" ) -func (p *Plugin) handleListProjects(w http.ResponseWriter, _ *http.Request) { - projects, err := p.store.ListProjects() +func (p *Plugin) handleListProjects(w http.ResponseWriter, r *http.Request) { + projects, err := p.listProjectsForRequest(r) if err != nil { respondError(w, http.StatusInternalServerError, err.Error()) return @@ -30,6 +30,7 @@ func (p *Plugin) handleCreateProject(w http.ResponseWriter, r *http.Request) { ID: uuid.New().String(), Name: req.Name, Prefix: normalizePrefix(req.Prefix), + Scope: projectScopeFromRequest(r), NextIssueNumber: 0, CreatedBy: getUserID(r), CreatedAt: nowMillis(), @@ -50,7 +51,7 @@ func (p *Plugin) handleCreateProject(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleGetProject(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - project, err := p.store.GetProject(id) + project, err := p.getProjectForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -61,7 +62,7 @@ func (p *Plugin) handleGetProject(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleUpdateProject(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - project, err := p.store.GetProject(id) + project, err := p.getProjectForRequest(r, id) if err != nil { respondError(w, http.StatusNotFound, err.Error()) return @@ -96,7 +97,7 @@ func (p *Plugin) handleUpdateProject(w http.ResponseWriter, r *http.Request) { func (p *Plugin) handleDeleteProject(w http.ResponseWriter, r *http.Request) { id := mux.Vars(r)["id"] - if _, err := p.store.GetProject(id); err != nil { + if _, err := p.getProjectForRequest(r, id); err != nil { respondError(w, http.StatusNotFound, err.Error()) return } diff --git a/mattermost-plugin-issues/server/api_search.go b/mattermost-plugin-issues/server/api_search.go index 78d2b16da49..b6b2980be79 100644 --- a/mattermost-plugin-issues/server/api_search.go +++ b/mattermost-plugin-issues/server/api_search.go @@ -17,6 +17,10 @@ func (p *Plugin) handleGetIssueByIdentifier(w http.ResponseWriter, r *http.Reque respondError(w, http.StatusNotFound, err.Error()) return } + if _, err := p.getProjectForRequest(r, issue.ProjectID); err != nil { + respondError(w, http.StatusNotFound, err.Error()) + return + } respondJSON(w, http.StatusOK, issue) } @@ -38,5 +42,12 @@ func (p *Plugin) handleSearchAllIssues(w http.ResponseWriter, r *http.Request) { respondError(w, http.StatusInternalServerError, err.Error()) return } - respondJSON(w, http.StatusOK, issues) + + filtered := make([]*Issue, 0, len(issues)) + for _, issue := range issues { + if _, err := p.getProjectForRequest(r, issue.ProjectID); err == nil { + filtered = append(filtered, issue) + } + } + respondJSON(w, http.StatusOK, filtered) } diff --git a/mattermost-plugin-issues/server/cleanup_seed_data.go b/mattermost-plugin-issues/server/cleanup_seed_data.go new file mode 100644 index 00000000000..f919d483426 --- /dev/null +++ b/mattermost-plugin-issues/server/cleanup_seed_data.go @@ -0,0 +1,114 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import "fmt" + +var legacySeedProjects = map[string]string{ + "Plataforma de Pagos": "PAGOS", + "App Móvil": "MOVIL", + "Panel de Administración": "ADMIN", + "Infraestructura": "INFRA", +} + +func (p *Plugin) cleanupSeedSampleData() { + projects, err := p.store.ListProjects() + if err != nil { + p.API.LogError("[SeedCleanup] failed to list projects", "error", err.Error()) + return + } + + var deletedProjects int + var deletedIssues int + var deletedLabels int + var deletedCycles int + + for _, project := range projects { + if !isLegacySeedProject(project) { + continue + } + + issues, err := p.store.ListIssues(project.ID, IssueFilterParams{}) + if err != nil { + p.API.LogError("[SeedCleanup] failed to list issues", "project", project.Name, "error", err.Error()) + return + } + for _, issue := range issues { + if err := p.store.DeleteIssue(issue.ID); err != nil { + p.API.LogError("[SeedCleanup] failed to delete issue", "project", project.Name, "issue", issue.Title, "error", err.Error()) + return + } + deletedIssues++ + } + + labels, err := p.store.ListLabels(project.ID) + if err != nil { + p.API.LogError("[SeedCleanup] failed to list labels", "project", project.Name, "error", err.Error()) + return + } + for _, label := range labels { + if err := p.store.DeleteLabel(label.ID); err != nil { + p.API.LogError("[SeedCleanup] failed to delete label", "project", project.Name, "label", label.Name, "error", err.Error()) + return + } + deletedLabels++ + } + + cycles, err := p.store.ListCycles(project.ID) + if err != nil { + p.API.LogError("[SeedCleanup] failed to list cycles", "project", project.Name, "error", err.Error()) + return + } + for _, cycle := range cycles { + if err := p.store.DeleteCycle(cycle.ID); err != nil { + p.API.LogError("[SeedCleanup] failed to delete cycle", "project", project.Name, "cycle", cycle.Name, "error", err.Error()) + return + } + deletedCycles++ + } + + if err := p.store.DeleteProject(project.ID); err != nil { + p.API.LogError("[SeedCleanup] failed to delete project", "project", project.Name, "error", err.Error()) + return + } + deletedProjects++ + } + + if deletedProjects == 0 { + return + } + + if err := p.clearLegacySeedCompanyInfo(); err != nil { + p.API.LogError("[SeedCleanup] failed to clear company info", "error", err.Error()) + return + } + + p.API.LogInfo("[SeedCleanup] removed legacy seed data", + "projects", fmt.Sprintf("%d", deletedProjects), + "issues", fmt.Sprintf("%d", deletedIssues), + "labels", fmt.Sprintf("%d", deletedLabels), + "cycles", fmt.Sprintf("%d", deletedCycles), + ) +} + +func isLegacySeedProject(project *Project) bool { + prefix, ok := legacySeedProjects[project.Name] + if !ok { + return false + } + + return project.Prefix == prefix && project.CreatedBy == "system" +} + +func (p *Plugin) clearLegacySeedCompanyInfo() error { + info, err := p.store.GetCompanyInfo() + if err != nil { + return err + } + if info == nil || info.Company.Name != "NovaPay" { + return nil + } + + return p.store.SetCompanyInfo(&CompanyInfo{}) +} diff --git a/mattermost-plugin-issues/server/model.go b/mattermost-plugin-issues/server/model.go index c03490a6c86..f15ca1333bb 100644 --- a/mattermost-plugin-issues/server/model.go +++ b/mattermost-plugin-issues/server/model.go @@ -66,6 +66,7 @@ type Project struct { ID string `json:"id"` Name string `json:"name"` Prefix string `json:"prefix"` + Scope string `json:"scope,omitempty"` NextIssueNumber int `json:"next_issue_number"` CreatedBy string `json:"created_by"` CreatedAt int64 `json:"created_at"` diff --git a/mattermost-plugin-issues/server/plugin.go b/mattermost-plugin-issues/server/plugin.go index 0d47159324e..5dc5018538c 100644 --- a/mattermost-plugin-issues/server/plugin.go +++ b/mattermost-plugin-issues/server/plugin.go @@ -69,9 +69,7 @@ func (p *Plugin) OnActivate() error { p.API.LogInfo("[ConversationMonitor] AI analysis disabled (missing configuration)") } - // Seed sample data (projects, issues, labels, cycles, company info). - // Idempotent — skips if projects already exist. - p.seedSampleData() + p.cleanupSeedSampleData() p.conversationMonitor = NewConversationMonitor(p.API, p.botUserID, p.oliAgentUserID, notifChannel.Id, p.onConversationEnd) @@ -159,9 +157,9 @@ func (p *Plugin) onConversationEnd(conv *conversationState, usernameCache map[st username = username[1:] } messages[i] = ConversationMessagePayload{ - UserID: msg.UserID, - Username: username, - Message: msg.Message, + UserID: msg.UserID, + Username: username, + Message: msg.Message, Timestamp: msg.Timestamp, } } diff --git a/mattermost-plugin-issues/server/project_scope.go b/mattermost-plugin-issues/server/project_scope.go new file mode 100644 index 00000000000..5adc7bb6e43 --- /dev/null +++ b/mattermost-plugin-issues/server/project_scope.go @@ -0,0 +1,101 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package main + +import ( + "fmt" + "net/http" + "strings" +) + +const projectScopeHeader = "X-Oli-Team-Scope" + +func normalizeProjectScope(scope string) string { + return strings.TrimSpace(strings.ToLower(scope)) +} + +func projectScopeFromRequest(r *http.Request) string { + if r == nil { + return "" + } + return normalizeProjectScope(r.Header.Get(projectScopeHeader)) +} + +func projectMatchesScope(project *Project, scope string) bool { + if project == nil { + return false + } + if scope == "" { + return true + } + return normalizeProjectScope(project.Scope) == scope +} + +func scopeNotFoundError(entity, id string) error { + return fmt.Errorf("%s not found: %s", entity, id) +} + +func (p *Plugin) listProjectsForRequest(r *http.Request) ([]*Project, error) { + projects, err := p.store.ListProjects() + if err != nil { + return nil, err + } + + scope := projectScopeFromRequest(r) + if scope == "" { + return projects, nil + } + + filtered := make([]*Project, 0, len(projects)) + for _, project := range projects { + if projectMatchesScope(project, scope) { + filtered = append(filtered, project) + } + } + return filtered, nil +} + +func (p *Plugin) getProjectForRequest(r *http.Request, id string) (*Project, error) { + project, err := p.store.GetProject(id) + if err != nil { + return nil, err + } + if !projectMatchesScope(project, projectScopeFromRequest(r)) { + return nil, scopeNotFoundError("project", id) + } + return project, nil +} + +func (p *Plugin) getIssueForRequest(r *http.Request, id string) (*Issue, error) { + issue, err := p.store.GetIssue(id) + if err != nil { + return nil, err + } + if _, err := p.getProjectForRequest(r, issue.ProjectID); err != nil { + return nil, scopeNotFoundError("issue", id) + } + return issue, nil +} + +func (p *Plugin) getLabelForRequest(r *http.Request, id string) (*IssueLabel, error) { + label, err := p.store.GetLabel(id) + if err != nil { + return nil, err + } + if _, err := p.getProjectForRequest(r, label.ProjectID); err != nil { + return nil, scopeNotFoundError("label", id) + } + return label, nil +} + +func (p *Plugin) getCycleForRequest(r *http.Request, id string) (*Cycle, error) { + cycle, err := p.store.GetCycle(id) + if err != nil { + return nil, err + } + if _, err := p.getProjectForRequest(r, cycle.ProjectID); err != nil { + return nil, scopeNotFoundError("cycle", id) + } + return cycle, nil +} diff --git a/mattermost-plugin-issues/server/seed_data.go b/mattermost-plugin-issues/server/seed_data.go deleted file mode 100644 index 9f0cf28dc1f..00000000000 --- a/mattermost-plugin-issues/server/seed_data.go +++ /dev/null @@ -1,492 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -package main - -import ( - "fmt" - "time" - - "github.com/google/uuid" -) - -// seedSampleData populates the store with realistic sample data for a -// Latin-American fintech company. It is idempotent — if any projects already -// exist it returns immediately. -func (p *Plugin) seedSampleData() { - existing, err := p.store.ListProjects() - if err != nil { - p.API.LogError("[Seed] failed to list projects", "error", err.Error()) - return - } - if len(existing) > 0 { - return - } - - p.API.LogInfo("[Seed] populating sample data …") - - now := time.Now() - baseTime := now.Add(-30 * 24 * time.Hour).UnixMilli() // 30 days ago - day := int64(24 * 60 * 60 * 1000) // 1 day in millis - - createdBy := "system" - - // --------------------------------------------------------------- - // Projects - // --------------------------------------------------------------- - projects := []*Project{ - {ID: uuid.New().String(), Name: "Plataforma de Pagos", Prefix: "PAGOS", CreatedBy: createdBy, CreatedAt: baseTime}, - {ID: uuid.New().String(), Name: "App Móvil", Prefix: "MOVIL", CreatedBy: createdBy, CreatedAt: baseTime + day}, - {ID: uuid.New().String(), Name: "Panel de Administración", Prefix: "ADMIN", CreatedBy: createdBy, CreatedAt: baseTime + 2*day}, - {ID: uuid.New().String(), Name: "Infraestructura", Prefix: "INFRA", CreatedBy: createdBy, CreatedAt: baseTime + 3*day}, - } - - for _, proj := range projects { - if err := p.store.CreateProject(proj); err != nil { - p.API.LogError("[Seed] project", "name", proj.Name, "error", err.Error()) - return - } - } - - pPagos := projects[0].ID - pMovil := projects[1].ID - pAdmin := projects[2].ID - pInfra := projects[3].ID - - // --------------------------------------------------------------- - // Labels (shared names, created per project) - // --------------------------------------------------------------- - type labelDef struct { - Name string - Color string - } - sharedLabels := []labelDef{ - {"Bug", "#e05c5c"}, - {"Mejora", "#3dc779"}, - {"Seguridad", "#e74c3c"}, - {"Rendimiento", "#f39c12"}, - {"UI/UX", "#9b59b6"}, - {"Documentación", "#3498db"}, - {"Deuda técnica", "#95a5a6"}, - } - - // labelMap[projectID][labelName] = labelID - labelMap := map[string]map[string]string{} - for _, proj := range projects { - labelMap[proj.ID] = map[string]string{} - for _, ld := range sharedLabels { - label := &IssueLabel{ - ID: uuid.New().String(), - ProjectID: proj.ID, - Name: ld.Name, - Color: ld.Color, - } - if err := p.store.CreateLabel(label); err != nil { - p.API.LogError("[Seed] label", "name", ld.Name, "project", proj.Name, "error", err.Error()) - } - labelMap[proj.ID][ld.Name] = label.ID - } - } - - // --------------------------------------------------------------- - // Cycles (sprints) - // --------------------------------------------------------------- - cycles := []*Cycle{ - {ID: uuid.New().String(), ProjectID: pPagos, Name: "Sprint 14 — Marzo", StartDate: "2026-03-02", EndDate: "2026-03-15", IsActive: false}, - {ID: uuid.New().String(), ProjectID: pPagos, Name: "Sprint 15 — Marzo", StartDate: "2026-03-16", EndDate: "2026-03-29", IsActive: true}, - {ID: uuid.New().String(), ProjectID: pMovil, Name: "Sprint 8 — Marzo", StartDate: "2026-03-02", EndDate: "2026-03-15", IsActive: false}, - {ID: uuid.New().String(), ProjectID: pMovil, Name: "Sprint 9 — Marzo", StartDate: "2026-03-16", EndDate: "2026-03-29", IsActive: true}, - {ID: uuid.New().String(), ProjectID: pAdmin, Name: "Sprint 5 — Marzo", StartDate: "2026-03-16", EndDate: "2026-03-29", IsActive: true}, - {ID: uuid.New().String(), ProjectID: pInfra, Name: "Q1 2026 — Infraestructura", StartDate: "2026-01-06", EndDate: "2026-03-31", IsActive: true}, - } - - cycleMap := map[string]string{} // name -> ID - for _, c := range cycles { - if err := p.store.CreateCycle(c); err != nil { - p.API.LogError("[Seed] cycle", "name", c.Name, "error", err.Error()) - } - cycleMap[c.Name] = c.ID - } - - // --------------------------------------------------------------- - // Team member user IDs (will be displayed as assignees) - // --------------------------------------------------------------- - members := []string{"martin.garcia", "lucia.fernandez", "santiago.lopez", "valentina.ruiz", "nicolas.martinez"} - - // Helpers - lb := func(projID, name string) string { return labelMap[projID][name] } - cy := func(name string) string { return cycleMap[name] } - - // --------------------------------------------------------------- - // Issues — Plataforma de Pagos - // --------------------------------------------------------------- - pagosIssues := []*Issue{ - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Error de redondeo en conversión USD→ARS", - Description: "Al procesar pagos internacionales, el monto final en ARS tiene diferencias de centavos respecto al tipo de cambio oficial del BCRA. Afecta a comercios con volumen alto.", - Status: IssueStatusInProgress, Priority: IssuePriorityUrgent, - LabelIDs: []string{lb(pPagos, "Bug"), lb(pPagos, "Seguridad")}, - AssigneeID: members[0], CycleID: cy("Sprint 15 — Marzo"), - EstimateHours: 8, CreatedBy: members[2], - CreatedAt: baseTime + 20*day, UpdatedAt: baseTime + 28*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Implementar webhook de notificación de pago exitoso", - Description: "Los comercios necesitan recibir un POST a su endpoint cuando un pago se acredita. Incluir firma HMAC-SHA256 para validación.", - Status: IssueStatusInReview, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pPagos, "Mejora")}, - AssigneeID: members[1], CycleID: cy("Sprint 15 — Marzo"), - EstimateHours: 13, CreatedBy: members[0], - CreatedAt: baseTime + 18*day, UpdatedAt: baseTime + 27*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Agregar soporte para pagos con QR Interoperable", - Description: "Integrar el estándar de QR interoperable de la Cámara de Pagos para permitir cobros desde cualquier billetera virtual.", - Status: IssueStatusTodo, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pPagos, "Mejora")}, - AssigneeID: members[3], CycleID: cy("Sprint 15 — Marzo"), - EstimateHours: 21, CreatedBy: members[0], - CreatedAt: baseTime + 22*day, UpdatedAt: baseTime + 22*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Timeout en consulta de estado de transferencia SPEI", - Description: "Las consultas al endpoint de SPEI tardan más de 30s en horario pico (14-16hs). Hay que implementar circuit breaker y cache de estados recientes.", - Status: IssueStatusInProgress, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pPagos, "Bug"), lb(pPagos, "Rendimiento")}, - AssigneeID: members[4], CycleID: cy("Sprint 15 — Marzo"), - EstimateHours: 5, CreatedBy: members[1], - CreatedAt: baseTime + 25*day, UpdatedAt: baseTime + 29*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Migrar procesamiento de lotes a cola asíncrona", - Description: "Actualmente los archivos de conciliación se procesan de forma síncrona. Mover a una cola SQS con workers independientes para escalar horizontalmente.", - Status: IssueStatusBacklog, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pPagos, "Mejora"), lb(pPagos, "Rendimiento")}, - EstimateHours: 34, CreatedBy: members[0], - CreatedAt: baseTime + 10*day, UpdatedAt: baseTime + 10*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Documentar API de conciliación v2", - Description: "Crear documentación OpenAPI 3.0 para los nuevos endpoints de conciliación automática. Incluir ejemplos de request/response y códigos de error.", - Status: IssueStatusTodo, Priority: IssuePriorityLow, - LabelIDs: []string{lb(pPagos, "Documentación")}, - AssigneeID: members[2], CycleID: cy("Sprint 15 — Marzo"), - EstimateHours: 5, CreatedBy: members[1], - CreatedAt: baseTime + 15*day, UpdatedAt: baseTime + 15*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Refactorizar módulo de validación de CBU/CVU", - Description: "El código actual tiene lógica duplicada entre la validación de CBU bancario y CVU de fintech. Unificar en un solo validador con strategy pattern.", - Status: IssueStatusDone, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pPagos, "Deuda técnica")}, - AssigneeID: members[0], CycleID: cy("Sprint 14 — Marzo"), - EstimateHours: 8, CreatedBy: members[0], - CreatedAt: baseTime + 5*day, UpdatedAt: baseTime + 14*day, CompletedAt: baseTime + 14*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Límite de transferencia no se actualiza al cambiar categoría de usuario", - Description: "Cuando un usuario pasa de categoría Básica a Verificada, el límite diario de transferencia no se actualiza hasta el día siguiente. Debería ser inmediato.", - Status: IssueStatusDone, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pPagos, "Bug")}, - AssigneeID: members[1], CycleID: cy("Sprint 14 — Marzo"), - EstimateHours: 3, CreatedBy: members[3], - CreatedAt: baseTime + 3*day, UpdatedAt: baseTime + 12*day, CompletedAt: baseTime + 12*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Implementar reintentos automáticos en pagos rechazados por saldo insuficiente", - Description: "Permitir configurar hasta 3 reintentos automáticos con backoff exponencial cuando el emisor rechaza por saldo insuficiente temporario.", - Status: IssueStatusBacklog, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pPagos, "Mejora")}, - EstimateHours: 13, CreatedBy: members[2], - CreatedAt: baseTime + 8*day, UpdatedAt: baseTime + 8*day, - }, - { - ID: uuid.New().String(), ProjectID: pPagos, - Title: "Auditoría de cumplimiento PCI DSS nivel 1", - Description: "Preparar la documentación y evidencia necesaria para la auditoría anual PCI DSS. Coordinar con el equipo de seguridad para las pruebas de penetración.", - Status: IssueStatusCancelled, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pPagos, "Seguridad"), lb(pPagos, "Documentación")}, - AssigneeID: members[4], - EstimateHours: 40, CreatedBy: members[0], - CreatedAt: baseTime + 2*day, UpdatedAt: baseTime + 20*day, CompletedAt: baseTime + 20*day, - }, - } - - // --------------------------------------------------------------- - // Issues — App Móvil - // --------------------------------------------------------------- - movilIssues := []*Issue{ - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "La app crashea al escanear QR con cámara trasera en Samsung Galaxy A series", - Description: "Múltiples reportes de crash en Samsung Galaxy A14/A34 al abrir el escáner de QR. El crash ocurre en la librería de cámara CameraX. Afecta al 12% de usuarios Android.", - Status: IssueStatusInProgress, Priority: IssuePriorityUrgent, - LabelIDs: []string{lb(pMovil, "Bug")}, - AssigneeID: members[3], CycleID: cy("Sprint 9 — Marzo"), - EstimateHours: 8, CreatedBy: members[1], - CreatedAt: baseTime + 26*day, UpdatedAt: baseTime + 29*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Implementar autenticación biométrica (Face ID / huella)", - Description: "Agregar opción de desbloqueo con biometría para acceder a la app y confirmar transferencias mayores a $50.000. Usar BiometricPrompt en Android y LocalAuthentication en iOS.", - Status: IssueStatusInReview, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pMovil, "Mejora"), lb(pMovil, "Seguridad")}, - AssigneeID: members[4], CycleID: cy("Sprint 9 — Marzo"), - EstimateHours: 21, CreatedBy: members[0], - CreatedAt: baseTime + 16*day, UpdatedAt: baseTime + 28*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Pantalla de historial de movimientos no carga más de 50 registros", - Description: "La paginación del historial se rompe después de la tercera página. El endpoint devuelve 200 OK pero con array vacío. Problema en el cálculo del offset.", - Status: IssueStatusTodo, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pMovil, "Bug")}, - AssigneeID: members[1], CycleID: cy("Sprint 9 — Marzo"), - EstimateHours: 3, CreatedBy: members[3], - CreatedAt: baseTime + 24*day, UpdatedAt: baseTime + 24*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Rediseño de la pantalla de envío de dinero", - Description: "Simplificar el flujo de envío de 5 pasos a 3. Agregar selector de contactos frecuentes, autocompletado de alias y confirmación con resumen visual.", - Status: IssueStatusInProgress, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pMovil, "UI/UX"), lb(pMovil, "Mejora")}, - AssigneeID: members[3], CycleID: cy("Sprint 9 — Marzo"), - EstimateHours: 13, CreatedBy: members[0], - CreatedAt: baseTime + 19*day, UpdatedAt: baseTime + 27*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Modo oscuro no aplica en la pantalla de ajustes", - Description: "La pantalla de configuración y ajustes de la cuenta mantiene el tema claro aunque el usuario tenga modo oscuro activado. Falta propagar el tema al componente SettingsScreen.", - Status: IssueStatusDone, Priority: IssuePriorityLow, - LabelIDs: []string{lb(pMovil, "Bug"), lb(pMovil, "UI/UX")}, - AssigneeID: members[1], CycleID: cy("Sprint 8 — Marzo"), - EstimateHours: 2, CreatedBy: members[4], - CreatedAt: baseTime + 5*day, UpdatedAt: baseTime + 10*day, CompletedAt: baseTime + 10*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Agregar notificaciones push para pagos recibidos", - Description: "Cuando un usuario recibe una transferencia, debe recibir una notificación push inmediata con el monto y el remitente. Integrar con Firebase Cloud Messaging y APNs.", - Status: IssueStatusDone, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pMovil, "Mejora")}, - AssigneeID: members[4], CycleID: cy("Sprint 8 — Marzo"), - EstimateHours: 8, CreatedBy: members[0], - CreatedAt: baseTime + 2*day, UpdatedAt: baseTime + 13*day, CompletedAt: baseTime + 13*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Optimizar tiempo de carga inicial de la app", - Description: "La app tarda 4.2s en promedio en cargar en dispositivos de gama media. Objetivo: bajar a menos de 2s. Lazy-load de módulos no críticos y precaching de datos del usuario.", - Status: IssueStatusBacklog, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pMovil, "Rendimiento")}, - EstimateHours: 21, CreatedBy: members[0], - CreatedAt: baseTime + 7*day, UpdatedAt: baseTime + 7*day, - }, - { - ID: uuid.New().String(), ProjectID: pMovil, - Title: "Soporte para lectura de tarjetas NFC en Android", - Description: "Permitir a los usuarios acercar su tarjeta física de débito al celular para vincularla automáticamente a la cuenta. Solo Android por ahora.", - Status: IssueStatusBacklog, Priority: IssuePriorityLow, - LabelIDs: []string{lb(pMovil, "Mejora")}, - EstimateHours: 34, CreatedBy: members[2], - CreatedAt: baseTime + 12*day, UpdatedAt: baseTime + 12*day, - }, - } - - // --------------------------------------------------------------- - // Issues — Panel de Administración - // --------------------------------------------------------------- - adminIssues := []*Issue{ - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Dashboard de métricas de transacciones en tiempo real", - Description: "Crear un dashboard con gráficos de volumen de transacciones, tasa de éxito/fallo, y monto total procesado. Actualización cada 30 segundos vía WebSocket.", - Status: IssueStatusInProgress, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pAdmin, "Mejora"), lb(pAdmin, "UI/UX")}, - AssigneeID: members[2], CycleID: cy("Sprint 5 — Marzo"), - EstimateHours: 21, CreatedBy: members[0], - CreatedAt: baseTime + 17*day, UpdatedAt: baseTime + 28*day, - }, - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Filtro de usuarios por estado KYC no funciona", - Description: "Al filtrar usuarios por estado 'Verificación pendiente' el panel muestra todos los usuarios sin filtrar. El query param `kyc_status` no se envía al backend.", - Status: IssueStatusTodo, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pAdmin, "Bug")}, - AssigneeID: members[2], CycleID: cy("Sprint 5 — Marzo"), - EstimateHours: 3, CreatedBy: members[1], - CreatedAt: baseTime + 23*day, UpdatedAt: baseTime + 23*day, - }, - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Exportar reporte de transacciones a CSV/Excel", - Description: "Los operadores necesitan exportar los resultados de búsqueda de transacciones a CSV o Excel. Incluir todos los campos visibles más el ID interno de la transacción.", - Status: IssueStatusTodo, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pAdmin, "Mejora")}, - AssigneeID: members[1], CycleID: cy("Sprint 5 — Marzo"), - EstimateHours: 5, CreatedBy: members[0], - CreatedAt: baseTime + 20*day, UpdatedAt: baseTime + 20*day, - }, - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Implementar roles y permisos granulares", - Description: "Actualmente todos los admins tienen acceso total. Necesitamos 3 roles: Operador (solo lectura), Supervisor (lectura + acciones), Admin (todo). Usar RBAC con middleware de permisos.", - Status: IssueStatusBacklog, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pAdmin, "Seguridad"), lb(pAdmin, "Mejora")}, - EstimateHours: 34, CreatedBy: members[0], - CreatedAt: baseTime + 5*day, UpdatedAt: baseTime + 5*day, - }, - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Vista detalle de comercio con historial de transacciones", - Description: "Al hacer clic en un comercio, abrir una vista con sus datos, documentación KYB, volumen mensual y las últimas 100 transacciones con filtros por estado y fecha.", - Status: IssueStatusDone, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pAdmin, "Mejora"), lb(pAdmin, "UI/UX")}, - AssigneeID: members[2], CycleID: cy("Sprint 5 — Marzo"), - EstimateHours: 13, CreatedBy: members[0], - CreatedAt: baseTime + 10*day, UpdatedAt: baseTime + 22*day, CompletedAt: baseTime + 22*day, - }, - { - ID: uuid.New().String(), ProjectID: pAdmin, - Title: "Log de auditoría de acciones administrativas", - Description: "Registrar todas las acciones que realizan los operadores: bloqueo de usuarios, reversión de transacciones, cambios de configuración. Incluir IP, timestamp y detalle del cambio.", - Status: IssueStatusBacklog, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pAdmin, "Seguridad")}, - EstimateHours: 13, CreatedBy: members[4], - CreatedAt: baseTime + 8*day, UpdatedAt: baseTime + 8*day, - }, - } - - // --------------------------------------------------------------- - // Issues — Infraestructura - // --------------------------------------------------------------- - infraIssues := []*Issue{ - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Migrar base de datos primaria a Aurora PostgreSQL", - Description: "La instancia RDS actual se queda sin IOPS en horario pico. Migrar a Aurora con read replicas para separar cargas de lectura/escritura. Planificar ventana de mantenimiento de 2hs.", - Status: IssueStatusInProgress, Priority: IssuePriorityUrgent, - LabelIDs: []string{lb(pInfra, "Mejora"), lb(pInfra, "Rendimiento")}, - AssigneeID: members[4], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 40, CreatedBy: members[0], - CreatedAt: baseTime + 5*day, UpdatedAt: baseTime + 29*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Configurar alertas de Datadog para latencia de API", - Description: "Crear alertas que disparen cuando el p95 de latencia supere 500ms o el p99 supere 2s. Notificar por Slack al canal #infra-alertas y por PagerDuty al on-call.", - Status: IssueStatusDone, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pInfra, "Mejora")}, - AssigneeID: members[4], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 5, CreatedBy: members[4], - CreatedAt: baseTime + 3*day, UpdatedAt: baseTime + 8*day, CompletedAt: baseTime + 8*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Automatizar rotación de secretos en AWS Secrets Manager", - Description: "Las credenciales de base de datos y API keys de terceros se rotan manualmente. Configurar rotación automática cada 90 días con Lambda y notificación al equipo.", - Status: IssueStatusTodo, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pInfra, "Seguridad")}, - AssigneeID: members[4], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 13, CreatedBy: members[0], - CreatedAt: baseTime + 15*day, UpdatedAt: baseTime + 15*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Pipeline de CI/CD tarda 25 minutos — optimizar", - Description: "El pipeline completo (lint + test + build + deploy a staging) tarda 25 min. Paralelizar etapas independientes, agregar cache de dependencias y usar runners más grandes.", - Status: IssueStatusInReview, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pInfra, "Rendimiento"), lb(pInfra, "Deuda técnica")}, - AssigneeID: members[0], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 8, CreatedBy: members[4], - CreatedAt: baseTime + 12*day, UpdatedAt: baseTime + 26*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Implementar disaster recovery con failover a us-west-2", - Description: "Actualmente corremos solo en us-east-1. Configurar réplica en us-west-2 con Route 53 health checks y failover automático. RTO objetivo: 15 minutos, RPO: 5 minutos.", - Status: IssueStatusBacklog, Priority: IssuePriorityMedium, - LabelIDs: []string{lb(pInfra, "Mejora"), lb(pInfra, "Seguridad")}, - EstimateHours: 55, CreatedBy: members[0], - CreatedAt: baseTime + 1*day, UpdatedAt: baseTime + 1*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Actualizar Kubernetes de 1.27 a 1.29", - Description: "El cluster EKS está en la versión 1.27 que sale de soporte en abril. Planificar upgrade a 1.29 con validación de compatibilidad de todos los charts de Helm.", - Status: IssueStatusTodo, Priority: IssuePriorityHigh, - LabelIDs: []string{lb(pInfra, "Deuda técnica")}, - AssigneeID: members[0], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 13, CreatedBy: members[4], - CreatedAt: baseTime + 18*day, UpdatedAt: baseTime + 18*day, - }, - { - ID: uuid.New().String(), ProjectID: pInfra, - Title: "Certificado TLS del dominio de sandbox venció", - Description: "El certificado de sandbox.novapay.com.ar expiró el 25/03. Renovar y configurar auto-renovación con cert-manager y Let's Encrypt.", - Status: IssueStatusDone, Priority: IssuePriorityUrgent, - LabelIDs: []string{lb(pInfra, "Bug"), lb(pInfra, "Seguridad")}, - AssigneeID: members[4], CycleID: cy("Q1 2026 — Infraestructura"), - EstimateHours: 2, CreatedBy: members[1], - CreatedAt: baseTime + 25*day, UpdatedAt: baseTime + 25*day, CompletedAt: baseTime + 25*day, - }, - } - - // Create all issues. - allIssues := [][]*Issue{pagosIssues, movilIssues, adminIssues, infraIssues} - projectNames := []string{"Plataforma de Pagos", "App Móvil", "Panel de Administración", "Infraestructura"} - totalCreated := 0 - for idx, batch := range allIssues { - for _, issue := range batch { - if _, err := p.store.CreateIssue(issue); err != nil { - p.API.LogError("[Seed] issue", "title", issue.Title, "project", projectNames[idx], "error", err.Error()) - } else { - totalCreated++ - } - } - } - - // --------------------------------------------------------------- - // Update company info with Spanish content. - // --------------------------------------------------------------- - _ = p.store.SetCompanyInfo(&CompanyInfo{ - Company: CompanyDetails{ - Name: "NovaPay", - Mission: "Democratizar el acceso a pagos digitales seguros y eficientes para comercios y usuarios en América Latina.", - Description: "NovaPay es una fintech que ofrece una plataforma integral de procesamiento de pagos. Permite a comercios aceptar pagos con tarjeta, transferencia bancaria, QR y billeteras virtuales desde una sola integración. La app móvil permite a usuarios enviar y recibir dinero al instante.", - TeamMembers: members, - }, - Repository: RepositoryDetails{ - URL: "https://github.com/novapay/platform", - Description: "Monorepo de la plataforma NovaPay: API de pagos, app móvil, panel de administración e infraestructura.", - TechStack: []string{"Go", "React", "React Native", "TypeScript", "PostgreSQL", "Redis", "Kubernetes", "AWS", "Terraform", "Datadog"}, - MainBranch: "main", - }, - State: CurrentState{ - Summary: "Plataforma en producción con +15.000 comercios activos. Foco actual en QR interoperable, mejoras de rendimiento y migración a Aurora.", - Phase: "growth", - ActiveProjects: 4, - TotalIssues: int(totalCreated), - OpenIssues: int(totalCreated - 7), // rough: done + cancelled count - }, - }) - - p.API.LogInfo("[Seed] sample data created", - "projects", fmt.Sprintf("%d", len(projects)), - "issues", fmt.Sprintf("%d", totalCreated), - "labels", fmt.Sprintf("%d", len(projects)*len(sharedLabels)), - "cycles", fmt.Sprintf("%d", len(cycles)), - ) -} diff --git a/package-lock.json b/package-lock.json index 35bea33a6e6..1ad8cc2f2cf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -166,7 +166,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.3.tgz", "integrity": "sha512-5FcvN1JHw2sHJChotgx8Ek0lyuh4kCKelgMTTqhYJJtloNvUfpAFMeNQUtdlIaktwrSV9LtCdqwk48wL2wBacQ==", "dev": true, - "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.24.2", @@ -2379,6 +2378,7 @@ "dev": true, "license": "BSD-2-Clause", "optional": true, + "peer": true, "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", @@ -2400,6 +2400,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -4038,7 +4039,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=8.0.0" } @@ -4060,7 +4060,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.2.0.tgz", "integrity": "sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==", "license": "Apache-2.0", - "peer": true, "engines": { "node": "^18.19.0 || >=20.6.0" }, @@ -4073,7 +4072,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz", "integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, @@ -4089,7 +4087,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.208.0.tgz", "integrity": "sha512-Eju0L4qWcQS+oXxi6pgh7zvE2byogAkcsVv0OjHF/97iOz1N/aKE6etSGowYkie+YA1uo6DNwdSxaaNnLvcRlA==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/api-logs": "0.208.0", "import-in-the-middle": "^2.0.0", @@ -4477,7 +4474,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz", "integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" @@ -4494,7 +4490,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz", "integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==", "license": "Apache-2.0", - "peer": true, "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/resources": "2.2.0", @@ -4512,7 +4507,6 @@ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.38.0.tgz", "integrity": "sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==", "license": "Apache-2.0", - "peer": true, "engines": { "node": ">=14" } @@ -5297,7 +5291,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz", "integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.4.0", "@typescript-eslint/types": "7.4.0", @@ -5696,7 +5689,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5750,7 +5742,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6872,7 +6863,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", @@ -7793,7 +7783,8 @@ "integrity": "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==", "dev": true, "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/cross-env": { "version": "7.0.3", @@ -8226,7 +8217,6 @@ "integrity": "sha512-IkGlOLfJ3q7y9iaDMnNSArDdPg3Ntx8Ps6aL7yTEIpL6znA+t5L/LRTAGFz1J/12hM/NiNEYg0LoBEheqGdZXw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "26.6.0", "builder-util": "26.4.1", @@ -8883,6 +8873,7 @@ "dev": true, "hasInstallScript": true, "license": "MIT", + "peer": true, "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", @@ -8903,6 +8894,7 @@ "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", @@ -8918,6 +8910,7 @@ "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, "license": "MIT", + "peer": true, "optionalDependencies": { "graceful-fs": "^4.1.6" } @@ -8928,6 +8921,7 @@ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 4.0.0" } @@ -9242,7 +9236,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9688,7 +9681,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz", "integrity": "sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ==", "dev": true, - "peer": true, "dependencies": { "array-includes": "^3.1.7", "array.prototype.findlast": "^1.2.4", @@ -9721,7 +9713,6 @@ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -16110,7 +16101,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.0.0", @@ -16244,6 +16234,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "commander": "^9.4.0" }, @@ -16261,6 +16252,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || >=14" } @@ -16565,7 +16557,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react/-/react-17.0.2.tgz", "integrity": "sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1" @@ -16596,7 +16587,6 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.2.tgz", "integrity": "sha512-s4h96KtLDUQlsENhMn1ar8t2bEa+q/YAtj8pPPdIjPDGBDIVNsrD9aXNWqspUe6AzKCIG0C1HZZLqLV7qpOBGA==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -17095,6 +17085,7 @@ "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "glob": "^7.1.3" }, @@ -17219,7 +17210,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.49.11.tgz", "integrity": "sha512-wvS/geXgHUGs6A/4ud5BFIWKO1nKd7wYIGimDk4q4GFkJicILActpv9ueMT4eRGSsp1BdKHuw1WwAHXbhsJELQ==", "dev": true, - "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -17313,7 +17303,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", "dev": true, - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -18210,6 +18199,7 @@ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -18454,7 +18444,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -18749,7 +18738,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19099,7 +19087,6 @@ "integrity": "sha512-QaNKAvGCDRh3wW1dsDjeMdDXwZm2vqq3zn6Pvq4rHOEOGSaUMgOOjG2Y9ZbIGzpfkJk9ZYTHpDqgDfeBDcnLaw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -19148,7 +19135,6 @@ "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.10.0.tgz", "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.5.0", "@webpack-cli/configtest": "^1.2.0", @@ -19560,8 +19546,7 @@ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz", "integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg==", "deprecated": "This package is now deprecated. Move to @xterm/xterm instead.", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/xterm-addon-fit": { "version": "0.8.0", diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index a7ca9064079..59bf2caf97f 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -57,7 +57,6 @@ import { AO_GIT_ACTION, AO_GET_GIT_STATUS, } from 'common/communication'; -import aoManager from 'main/aoManager'; import Config from 'common/config'; import buildConfig from 'common/config/buildConfig'; import {MATTERMOST_PROTOCOL} from 'common/constants'; @@ -66,6 +65,7 @@ import ServerManager from 'common/servers/serverManager'; import {parseURL} from 'common/utils/url'; import {setTestField} from 'common/utils/util'; import ViewManager from 'common/views/viewManager'; +import aoManager from 'main/aoManager'; import AppVersionManager from 'main/AppVersionManager'; import AutoLauncher from 'main/AutoLauncher'; import {configPath, updatePaths} from 'main/constants'; @@ -126,6 +126,23 @@ import { handleGetDarkMode, } from './windows'; +const getIssuesRequestTeamScope = () => { + const currentView = TabManager.getCurrentActiveTabView(); + const pathname = currentView?.currentURL?.pathname || ''; + const pathParts = pathname.split('/').filter(Boolean); + if (pathParts.length > 0) { + return pathParts[0].trim().toLowerCase(); + } + + const currentTab = TabManager.getCurrentActiveTab(); + const teamName = currentTab?.title.teamName?.trim().toLowerCase(); + if (!teamName) { + return ''; + } + + return teamName.replace(/\s+/g, '-'); +}; + const log = new Logger('App.Initialize'); /** @@ -321,7 +338,8 @@ function initializeInterCommunicationEventListeners() { ipcMain.handle(ISSUES_API_REQUEST, async (_event, method: string, path: string, body?: unknown) => { const currentServerId = ServerManager.getCurrentServerId(); - console.log('[issues-api-request] incoming', {method, path, hasBody: body !== undefined, currentServerId}); + const teamScope = getIssuesRequestTeamScope(); + console.log('[issues-api-request] incoming', {method, path, hasBody: body !== undefined, currentServerId, teamScope}); const currentServer = currentServerId ? ServerManager.getServer(currentServerId) : null; if (!currentServer?.url) { @@ -341,7 +359,10 @@ function initializeInterCommunicationEventListeners() { 'X-Requested-With': 'XMLHttpRequest', }; if (token) { - headers['Authorization'] = `Bearer ${token}`; + headers.Authorization = `Bearer ${token}`; + } + if (teamScope) { + headers['X-Oli-Team-Scope'] = teamScope; } const response = await net.fetch(fullUrl, {