Add automatic Gitea label creation and repository edit functionality
- Implement automatic label creation when registering repositories:
• bzzz-task (red) - Issues for CHORUS BZZZ task assignments
• whoosh-monitored (teal) - Repository monitoring indicator
• priority-high/medium/low labels for task prioritization
- Add repository edit modal with full configuration options
- Add manual "Labels" button to ensure labels for existing repos
- Enhance Gitea client with CreateLabel, GetLabels, EnsureRequiredLabels methods
- Add POST /api/v1/repositories/{id}/ensure-labels endpoint
- Fix label creation error handling with graceful degradation
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/chorus-services/whoosh/internal/config"
|
||||
"github.com/chorus-services/whoosh/internal/database"
|
||||
"github.com/chorus-services/whoosh/internal/gitea"
|
||||
"github.com/chorus-services/whoosh/internal/monitor"
|
||||
"github.com/chorus-services/whoosh/internal/p2p"
|
||||
"github.com/chorus-services/whoosh/internal/tasks"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -38,6 +39,7 @@ type Server struct {
|
||||
teamComposer *composer.Service
|
||||
taskService *tasks.Service
|
||||
giteaIntegration *tasks.GiteaIntegration
|
||||
repoMonitor *monitor.Monitor
|
||||
}
|
||||
|
||||
func NewServer(cfg *config.Config, db *database.DB) (*Server, error) {
|
||||
@@ -45,6 +47,9 @@ func NewServer(cfg *config.Config, db *database.DB) (*Server, error) {
|
||||
taskService := tasks.NewService(db.Pool)
|
||||
giteaIntegration := tasks.NewGiteaIntegration(taskService, gitea.NewClient(cfg.GITEA), nil)
|
||||
|
||||
// Initialize repository monitor
|
||||
repoMonitor := monitor.NewMonitor(db.Pool, cfg.GITEA)
|
||||
|
||||
s := &Server{
|
||||
config: cfg,
|
||||
db: db,
|
||||
@@ -54,6 +59,7 @@ func NewServer(cfg *config.Config, db *database.DB) (*Server, error) {
|
||||
teamComposer: composer.NewService(db.Pool, nil), // Use default config
|
||||
taskService: taskService,
|
||||
giteaIntegration: giteaIntegration,
|
||||
repoMonitor: repoMonitor,
|
||||
}
|
||||
|
||||
// Initialize BACKBEAT integration if enabled
|
||||
@@ -173,6 +179,7 @@ func (s *Server) setupRoutes() {
|
||||
r.Put("/{repoID}", s.updateRepositoryHandler)
|
||||
r.Delete("/{repoID}", s.deleteRepositoryHandler)
|
||||
r.Post("/{repoID}/sync", s.syncRepositoryHandler)
|
||||
r.Post("/{repoID}/ensure-labels", s.ensureRepositoryLabelsHandler)
|
||||
r.Get("/{repoID}/logs", s.getRepositorySyncLogsHandler)
|
||||
})
|
||||
|
||||
@@ -199,6 +206,16 @@ func (s *Server) Start(ctx context.Context) error {
|
||||
return fmt.Errorf("failed to start P2P discovery: %w", err)
|
||||
}
|
||||
|
||||
// Start repository monitoring service
|
||||
if s.repoMonitor != nil {
|
||||
go func() {
|
||||
if err := s.repoMonitor.Start(ctx); err != nil && err != context.Canceled {
|
||||
log.Error().Err(err).Msg("Repository monitoring service failed")
|
||||
}
|
||||
}()
|
||||
log.Info().Msg("🔍 Repository monitoring service started")
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("addr", s.httpServer.Addr).
|
||||
Msg("HTTP server starting")
|
||||
@@ -224,6 +241,12 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||
if err := s.p2pDiscovery.Stop(); err != nil {
|
||||
log.Error().Err(err).Msg("Failed to stop P2P discovery service")
|
||||
}
|
||||
|
||||
// Stop repository monitoring service
|
||||
if s.repoMonitor != nil {
|
||||
s.repoMonitor.Stop()
|
||||
log.Info().Msg("🛑 Repository monitoring service stopped")
|
||||
}
|
||||
|
||||
if err := s.httpServer.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("server shutdown failed: %w", err)
|
||||
@@ -2535,7 +2558,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
'<div style="border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; margin-bottom: 12px; display: flex; justify-content: space-between; align-items: center;">' +
|
||||
'<div style="flex: 1;">' +
|
||||
'<div style="display: flex; align-items: center; margin-bottom: 8px;">' +
|
||||
'<h4 style="margin: 0; color: #2d3748;">' + repo.full_name + '</h4>' +
|
||||
'<h4 style="margin: 0; color: #2d3748;"><a href="' + repo.url + '" target="_blank" style="color: #2d3748; text-decoration: none; cursor: pointer;" onmouseover="this.style.color=\'#4299e1\'" onmouseout="this.style.color=\'#2d3748\'">' + repo.full_name + '</a></h4>' +
|
||||
'<span style="margin-left: 12px; padding: 2px 8px; background: ' + getStatusColor(repo.sync_status) + '; color: white; border-radius: 12px; font-size: 12px; font-weight: 500;">' +
|
||||
repo.sync_status +
|
||||
'</span>' +
|
||||
@@ -2553,6 +2576,9 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
'<button onclick="syncRepository(\'' + repo.id + '\')" style="background: #4299e1; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
|
||||
'Sync' +
|
||||
'</button>' +
|
||||
'<button onclick="ensureLabels(\'' + repo.id + '\')" style="background: #38a169; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
|
||||
'Labels' +
|
||||
'</button>' +
|
||||
'<button onclick="editRepository(\'' + repo.id + '\')" style="background: #ed8936; color: white; border: none; padding: 6px 12px; border-radius: 4px; cursor: pointer; font-size: 12px;">' +
|
||||
'Edit' +
|
||||
'</button>' +
|
||||
@@ -2591,9 +2617,183 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
});
|
||||
}
|
||||
|
||||
function ensureLabels(repoId) {
|
||||
fetch('/api/v1/repositories/' + repoId + '/ensure-labels', {
|
||||
method: 'POST'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.error) {
|
||||
alert('Error ensuring labels: ' + data.error);
|
||||
} else {
|
||||
alert('Labels ensured successfully for ' + data.owner + '/' + data.name + '\\n\\nRequired labels created:\\n• bzzz-task\\n• whoosh-monitored\\n• priority-high\\n• priority-medium\\n• priority-low');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error ensuring labels:', error);
|
||||
alert('Error ensuring labels');
|
||||
});
|
||||
}
|
||||
|
||||
function editRepository(repoId) {
|
||||
// For MVP, just show an alert. In production, this would open an edit form
|
||||
alert('Edit functionality will be implemented. Repository ID: ' + repoId);
|
||||
// Fetch repository details first
|
||||
fetch('/api/v1/repositories/' + repoId)
|
||||
.then(response => response.json())
|
||||
.then(repo => {
|
||||
showEditModal(repo);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error fetching repository:', error);
|
||||
alert('Error fetching repository details');
|
||||
});
|
||||
}
|
||||
|
||||
function showEditModal(repo) {
|
||||
// Create modal overlay
|
||||
const overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; ' +
|
||||
'background: rgba(0,0,0,0.5); display: flex; align-items: center; ' +
|
||||
'justify-content: center; z-index: 1000;';
|
||||
|
||||
// Create modal content
|
||||
const modal = document.createElement('div');
|
||||
modal.style.cssText = 'background: white; border-radius: 8px; padding: 24px; ' +
|
||||
'max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;';
|
||||
|
||||
modal.innerHTML =
|
||||
'<h3 style="margin: 0 0 20px 0; color: #2d3748;">Edit Repository</h3>' +
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<strong>' + repo.full_name + '</strong>' +
|
||||
'<div style="font-size: 12px; color: #718096;">ID: ' + repo.id + '</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<form id="editRepoForm">' +
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Description:</label>' +
|
||||
'<input type="text" id="description" value="' + (repo.description || '') + '" ' +
|
||||
'style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px;">' +
|
||||
'</div>' +
|
||||
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Default Branch:</label>' +
|
||||
'<input type="text" id="defaultBranch" value="' + (repo.default_branch || 'main') + '" ' +
|
||||
'style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px;">' +
|
||||
'</div>' +
|
||||
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<label style="display: block; margin-bottom: 4px; font-weight: bold;">Language:</label>' +
|
||||
'<input type="text" id="language" value="' + (repo.language || '') + '" ' +
|
||||
'style="width: 100%; padding: 8px; border: 1px solid #e2e8f0; border-radius: 4px;">' +
|
||||
'</div>' +
|
||||
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<h4 style="margin: 0 0 8px 0;">Monitoring Options:</h4>' +
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center;">' +
|
||||
'<input type="checkbox" id="monitorIssues" ' + (repo.monitor_issues ? 'checked' : '') + ' style="margin-right: 8px;">' +
|
||||
'Monitor Issues' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center;">' +
|
||||
'<input type="checkbox" id="monitorPRs" ' + (repo.monitor_pull_requests ? 'checked' : '') + ' style="margin-right: 8px;">' +
|
||||
'Monitor Pull Requests' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center;">' +
|
||||
'<input type="checkbox" id="monitorReleases" ' + (repo.monitor_releases ? 'checked' : '') + ' style="margin-right: 8px;">' +
|
||||
'Monitor Releases' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div style="margin-bottom: 16px;">' +
|
||||
'<h4 style="margin: 0 0 8px 0;">CHORUS Integration:</h4>' +
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center;">' +
|
||||
'<input type="checkbox" id="enableChorus" ' + (repo.enable_chorus_integration ? 'checked' : '') + ' style="margin-right: 8px;">' +
|
||||
'Enable CHORUS Integration' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'<div style="margin-bottom: 8px;">' +
|
||||
'<label style="display: flex; align-items: center;">' +
|
||||
'<input type="checkbox" id="autoAssignTeams" ' + (repo.auto_assign_teams ? 'checked' : '') + ' style="margin-right: 8px;">' +
|
||||
'Auto-assign Teams' +
|
||||
'</label>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
|
||||
'<div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 24px;">' +
|
||||
'<button type="button" onclick="closeEditModal()" ' +
|
||||
'style="background: #e2e8f0; color: #4a5568; border: none; padding: 10px 16px; border-radius: 4px; cursor: pointer;">' +
|
||||
'Cancel' +
|
||||
'</button>' +
|
||||
'<button type="submit" ' +
|
||||
'style="background: #4299e1; color: white; border: none; padding: 10px 16px; border-radius: 4px; cursor: pointer;">' +
|
||||
'Save Changes' +
|
||||
'</button>' +
|
||||
'</div>' +
|
||||
'</form>';
|
||||
|
||||
overlay.appendChild(modal);
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// Store modal reference globally so we can close it
|
||||
window.currentEditModal = overlay;
|
||||
window.currentRepoId = repo.id;
|
||||
|
||||
// Handle form submission
|
||||
document.getElementById('editRepoForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
saveRepositoryChanges();
|
||||
});
|
||||
|
||||
// Close modal on overlay click
|
||||
overlay.addEventListener('click', function(e) {
|
||||
if (e.target === overlay) {
|
||||
closeEditModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeEditModal() {
|
||||
if (window.currentEditModal) {
|
||||
document.body.removeChild(window.currentEditModal);
|
||||
window.currentEditModal = null;
|
||||
window.currentRepoId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function saveRepositoryChanges() {
|
||||
const formData = {
|
||||
description: document.getElementById('description').value.trim() || null,
|
||||
default_branch: document.getElementById('defaultBranch').value.trim() || null,
|
||||
language: document.getElementById('language').value.trim() || null,
|
||||
monitor_issues: document.getElementById('monitorIssues').checked,
|
||||
monitor_pull_requests: document.getElementById('monitorPRs').checked,
|
||||
monitor_releases: document.getElementById('monitorReleases').checked,
|
||||
enable_chorus_integration: document.getElementById('enableChorus').checked,
|
||||
auto_assign_teams: document.getElementById('autoAssignTeams').checked
|
||||
};
|
||||
|
||||
fetch('/api/v1/repositories/' + window.currentRepoId, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
alert('Repository updated successfully!');
|
||||
closeEditModal();
|
||||
loadRepositories(); // Reload the list to show changes
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error updating repository:', error);
|
||||
alert('Error updating repository');
|
||||
});
|
||||
}
|
||||
|
||||
function deleteRepository(repoId, fullName) {
|
||||
@@ -2879,6 +3079,26 @@ func (s *Server) createRepositoryHandler(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// Automatically create required labels in the Gitea repository
|
||||
if req.SourceType == "gitea" && s.repoMonitor != nil && s.repoMonitor.GetGiteaClient() != nil {
|
||||
log.Info().
|
||||
Str("repository", fullName).
|
||||
Msg("Creating required labels in Gitea repository")
|
||||
|
||||
err := s.repoMonitor.GetGiteaClient().EnsureRequiredLabels(context.Background(), req.Owner, req.Name)
|
||||
if err != nil {
|
||||
log.Warn().
|
||||
Err(err).
|
||||
Str("repository", fullName).
|
||||
Msg("Failed to create labels in Gitea repository - repository monitoring will still work")
|
||||
// Don't fail the entire request if label creation fails
|
||||
} else {
|
||||
log.Info().
|
||||
Str("repository", fullName).
|
||||
Msg("Successfully created required labels in Gitea repository")
|
||||
}
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusCreated)
|
||||
render.JSON(w, r, map[string]interface{}{
|
||||
"id": id,
|
||||
@@ -3088,13 +3308,85 @@ func (s *Server) syncRepositoryHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
log.Info().Str("repository_id", repoID).Msg("Manual repository sync triggered")
|
||||
|
||||
// TODO: Implement repository sync logic
|
||||
// This would trigger the Gitea issue monitoring service
|
||||
if s.repoMonitor == nil {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, map[string]string{"error": "repository monitoring service not available"})
|
||||
return
|
||||
}
|
||||
|
||||
// Trigger repository sync in background
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
if err := s.repoMonitor.SyncRepository(ctx, repoID); err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("repository_id", repoID).
|
||||
Msg("Manual repository sync failed")
|
||||
}
|
||||
}()
|
||||
|
||||
render.JSON(w, r, map[string]interface{}{
|
||||
"message": "Repository sync triggered",
|
||||
"message": "Repository sync triggered",
|
||||
"repository_id": repoID,
|
||||
"status": "pending",
|
||||
"status": "started",
|
||||
})
|
||||
}
|
||||
|
||||
// ensureRepositoryLabelsHandler ensures required labels exist in the Gitea repository
|
||||
func (s *Server) ensureRepositoryLabelsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
repoID := chi.URLParam(r, "repoID")
|
||||
|
||||
log.Info().Str("repository_id", repoID).Msg("Ensuring repository labels")
|
||||
|
||||
if s.repoMonitor == nil || s.repoMonitor.GetGiteaClient() == nil {
|
||||
render.Status(r, http.StatusServiceUnavailable)
|
||||
render.JSON(w, r, map[string]string{"error": "repository monitoring service not available"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get repository details first
|
||||
query := "SELECT owner, name FROM repositories WHERE id = $1"
|
||||
var owner, name string
|
||||
err := s.db.Pool.QueryRow(context.Background(), query, repoID).Scan(&owner, &name)
|
||||
if err != nil {
|
||||
if err.Error() == "no rows in result set" {
|
||||
render.Status(r, http.StatusNotFound)
|
||||
render.JSON(w, r, map[string]string{"error": "repository not found"})
|
||||
return
|
||||
}
|
||||
log.Error().Err(err).Msg("Failed to get repository")
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": "failed to get repository"})
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure required labels exist
|
||||
err = s.repoMonitor.GetGiteaClient().EnsureRequiredLabels(context.Background(), owner, name)
|
||||
if err != nil {
|
||||
log.Error().
|
||||
Err(err).
|
||||
Str("repository_id", repoID).
|
||||
Str("owner", owner).
|
||||
Str("name", name).
|
||||
Msg("Failed to ensure repository labels")
|
||||
render.Status(r, http.StatusInternalServerError)
|
||||
render.JSON(w, r, map[string]string{"error": "failed to create labels: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("repository_id", repoID).
|
||||
Str("owner", owner).
|
||||
Str("name", name).
|
||||
Msg("Successfully ensured repository labels")
|
||||
|
||||
render.JSON(w, r, map[string]interface{}{
|
||||
"message": "Repository labels ensured successfully",
|
||||
"repository_id": repoID,
|
||||
"owner": owner,
|
||||
"name": name,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user