[Git][root/cli/mirror-monitor][main] 2 commits: feat: add urls, time and ports as args

Heric Camargo pushed to branch main at Root / CLI / Mirror Monitor Commits: 3797536c by Heric Camargo at 2025-08-01T14:49:28-03:00 feat: add urls, time and ports as args - - - - - 96b47d68 by Heric Camargo at 2025-08-01T14:50:12-03:00 build: add mage to the build system - - - - - 8 changed files: - .gitignore - cmd/api/main.go - cmd/tui/main.go - go.mod - go.sum - + mage.go - + magefile.go - shell.nix Changes: ===================================== .gitignore ===================================== @@ -5,6 +5,7 @@ # binaries rsyncuptime-server rsyncuptime-tui +bin/ # Binaries for programs and plugins *.exe @@ -31,7 +32,10 @@ go.work.sum # env file .env +.envrc # Editor/IDE # .idea/ # .vscode/ +// Ignore Mage generated main file +mage_output_file.go ===================================== cmd/api/main.go ===================================== @@ -263,64 +263,72 @@ func main() { mux := http.NewServeMux() // Handler for the root endpoint, listing available modules. - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.URL.Path != "/" { - writeJSONError(w, http.StatusNotFound, "Endpoint not found. See / for available modules.", r.URL.Path) + // GET /modules - list all monitored modules + mux.HandleFunc("/modules", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed", r.URL.Path) return } - w.Header().Set("Content-Type", "application/json") - endpoints := make(map[string]string) - for _, module := range discoveredModules { - endpoints[module] = fmt.Sprintf("/status/%s", module) - } - - // Tenta obter a lista de diretórios do rsync - var rsyncDirs []string - out, err := execCommand("rsync", rsyncURL).CombinedOutput() - if err == nil { - scanner := bufio.NewScanner(strings.NewReader(string(out))) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if line == "" { - continue - } - // Pega o nome do diretório (primeira palavra) - parts := strings.Fields(line) - if len(parts) > 0 { - rsyncDirs = append(rsyncDirs, parts[0]) - } - } - } - json.NewEncoder(w).Encode(map[string]interface{}{ - "path": "/", - "success": true, - "message": "Monitoring all discovered modules. See endpoints below.", - "monitored_modules": endpoints, + "modules": discoveredModules, "polling_interval_s": pollingInterval.Seconds(), - "rsync_directories": rsyncDirs, }) }) - // A single handler for all /status/ requests that validates input. - mux.HandleFunc("/status/", func(w http.ResponseWriter, r *http.Request) { - module := strings.TrimPrefix(r.URL.Path, "/status/") + // Router for module-specific endpoints: GET /modules/{module} and /modules/{module}/history + mux.HandleFunc("/modules/", func(w http.ResponseWriter, r *http.Request) { + path := strings.TrimPrefix(r.URL.Path, "/modules/") + parts := strings.Split(path, "/") + module := parts[0] if module == "" { - writeJSONError(w, http.StatusBadRequest, "Module name cannot be empty. Path should be /status/<module-name>.", r.URL.Path) + writeJSONError(w, http.StatusBadRequest, "Module name cannot be empty. Path should be /modules/{module}", r.URL.Path) return } if !isValidModulePath(module) { - writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Nome de módulo inválido: '%s'. Permitidos apenas letras, números, hífen, underline e ponto. Exemplo válido: debian-archive. Consulte a documentação.", module), r.URL.Path) + writeJSONError(w, http.StatusBadRequest, fmt.Sprintf("Invalid module name: '%s'", module), r.URL.Path) return } - checker, found := checkers[module] if !found { - writeJSONError(w, http.StatusNotFound, fmt.Sprintf("Module '%s' is not monitored.", module), r.URL.Path) + writeJSONError(w, http.StatusNotFound, fmt.Sprintf("Module '%s' not found", module), r.URL.Path) + return + } + // GET /modules/{module} -> latest status + if len(parts) == 1 { + if r.Method != http.MethodGet { + writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed", r.URL.Path) + return + } + checker.mu.RLock() + defer checker.mu.RUnlock() + if len(checker.results) == 0 { + writeJSONError(w, http.StatusNoContent, "No status available", r.URL.Path) + return + } + latest := checker.results[len(checker.results)-1] + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(latest.HTTPStatus) + json.NewEncoder(w).Encode(latest) + return + } + // GET /modules/{module}/history -> full history + if len(parts) == 2 && parts[1] == "history" { + if r.Method != http.MethodGet { + writeJSONError(w, http.StatusMethodNotAllowed, "Method not allowed", r.URL.Path) + return + } + checker.ServeHTTP(w, r) return } - checker.ServeHTTP(w, r) + writeJSONError(w, http.StatusNotFound, "Endpoint not found", r.URL.Path) + }) + + // A single handler for all /status/ requests that validates input. + // legacy /status/ redirect to new endpoints + mux.HandleFunc("/status/", func(w http.ResponseWriter, r *http.Request) { + http.Redirect(w, r, strings.Replace(r.URL.Path, "/status/", "/modules/", 1), http.StatusMovedPermanently) + return }) log.Printf("Starting monitoring server on :%s using rsync URL '%s'", serverPort, rsyncURL) ===================================== cmd/tui/main.go ===================================== @@ -1,27 +1,47 @@ package main import ( - "encoding/json" - "fmt" - "io" - "log" - "net/http" - "os" - "sort" - "strings" - "sync" - "time" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "os" + "sort" + "strings" + "sync" + "time" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // Configuration -const ( - apiBaseURL = "http://localhost:8080" - refreshInterval = 1 * time.Minute + +var ( + apiBaseURL = getAPIBaseURL() + refreshInterval = getRefreshInterval() ) +// getRefreshInterval returns the refresh interval from the environment or defaults to 1 minute. +func getRefreshInterval() time.Duration { + if val := os.Getenv("REFRESH_INTERVAL_SECONDS"); val != "" { + if sec, err := time.ParseDuration(val + "s"); err == nil && sec > 0 { + return sec + } + log.Printf("WARN: Invalid REFRESH_INTERVAL_SECONDS value '%s'. Using default.", val) + } + return 1 * time.Minute +} + +// getAPIBaseURL returns the API base URL from the environment or defaults to localhost. +func getAPIBaseURL() string { + if url := os.Getenv("API_BASE_URL"); url != "" { + return url + } + return "http://localhost:8080" +} + // historyBarWidth agora é dinâmico, depende do tamanho do terminal var ( @@ -69,15 +89,15 @@ func initialModel() model { // --- Bubble Tea Commands --- func fetchStatuses() tea.Cmd { return func() tea.Msg { - resp, err := http.Get(apiBaseURL + "/") + resp, err := http.Get(apiBaseURL + "/modules") if err != nil { return errMsg{err} } defer resp.Body.Close() - var discoveryResponse struct { - Modules map[string]string `json:"monitored_modules"` - } + var discoveryResponse struct { + Modules []string `json:"modules"` + } if err := json.NewDecoder(resp.Body).Decode(&discoveryResponse); err != nil { return errMsg{err} } @@ -86,7 +106,7 @@ func fetchStatuses() tea.Cmd { var mu sync.Mutex var wg sync.WaitGroup - for name := range discoveryResponse.Modules { + for _, name := range discoveryResponse.Modules { wg.Add(1) go func(moduleName string) { defer wg.Done() @@ -107,7 +127,8 @@ func fetchStatuses() tea.Cmd { } func fetchModuleHistory(name string) ([]CheckResult, error) { - resp, err := http.Get(fmt.Sprintf("%s/status/%s", apiBaseURL, name)) + // fetch full history for module via new RESTful endpoint + resp, err := http.Get(fmt.Sprintf("%s/modules/%s/history", apiBaseURL, name)) if err != nil { return nil, err } @@ -299,9 +320,9 @@ func renderHistoryBar(history []CheckResult, width int) string { if totalChecks <= width { for _, check := range history { if check.IsUp { - b.WriteString(statusUpStyle.Render("█")) + b.WriteString(statusUpStyle.Render("●")) } else { - b.WriteString(statusDownStyle.Render("█")) + b.WriteString(statusDownStyle.Render("●")) } } b.WriteString(strings.Repeat(" ", width-totalChecks)) // Pad with space @@ -333,9 +354,9 @@ func renderHistoryBar(history []CheckResult, width int) string { } if isUp { - b.WriteString(statusUpStyle.Render("█")) + b.WriteString(statusUpStyle.Render("●")) } else { - b.WriteString(statusDownStyle.Render("█")) + b.WriteString(statusDownStyle.Render("●")) } } return b.String() ===================================== go.mod ===================================== @@ -13,6 +13,7 @@ require ( github.com/charmbracelet/x/term v0.2.1 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magefile/mage v1.15.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect ===================================== go.sum ===================================== @@ -18,6 +18,8 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= +github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= ===================================== mage.go ===================================== @@ -0,0 +1,8 @@ + +package main + +import "github.com/magefile/mage/mage" + +func main() { + mage.Main() +} ===================================== magefile.go ===================================== @@ -0,0 +1,32 @@ +//go:build mage +// +build mage + +package main + +import ( + "fmt" + "os" + "os/exec" +) + +// Build builds both the API and TUI binaries. +func Build() error { + fmt.Println("Building API...") + apiCmd := exec.Command("go", "build", "-o", "bin/api", "./cmd/api") + apiCmd.Stdout = os.Stdout + apiCmd.Stderr = os.Stderr + if err := apiCmd.Run(); err != nil { + return fmt.Errorf("failed to build API: %w", err) + } + + fmt.Println("Building TUI...") + tuiCmd := exec.Command("go", "build", "-o", "bin/tui", "./cmd/tui") + tuiCmd.Stdout = os.Stdout + tuiCmd.Stderr = os.Stderr + if err := tuiCmd.Run(); err != nil { + return fmt.Errorf("failed to build TUI: %w", err) + } + + fmt.Println("Build complete.") + return nil +} ===================================== shell.nix ===================================== @@ -7,6 +7,7 @@ pkgs.mkShell { pkgs.go pkgs.rsync pkgs.jq + pkgs.mage ]; shellHook = '' View it on GitLab: https://gitlab.c3sl.ufpr.br/root/cli/mirror-monitor/-/compare/296d334a7f0183... -- View it on GitLab: https://gitlab.c3sl.ufpr.br/root/cli/mirror-monitor/-/compare/296d334a7f0183... You're receiving this email because of your account on gitlab.c3sl.ufpr.br.
participantes (1)
-
Heric Camargo (@hc20)