Files
egutierrez 2a3d780347 feat(doctor): add fn doctor CLI + 14 functions for system management
Adds `fn doctor` read-only diagnostic command with subcommands artefacts,
services, sync, uses-functions, unused, and --json flag for agents.
Each subcommand wraps a registry function in functions/infra/.

New functions:
- artefact_doctor, services_status, pc_locations_drift,
  audit_uses_functions, find_unused_functions (Go diagnostics)
- backup_sqlite_db, rotate_backups, wait_for_http, wait_for_port,
  port_kill, tail_journal, pre_commit_hook_install (bash utilities)
- notify_telegram (Go HTTP)
- backup_all pipeline (tag launcher)

Plus prior session leftovers (scan_secrets_in_dirty, append_diary_entry,
git utilities, http_session_cookie_middleware, compile/full-git pipelines).

Fixes pc_locations_drift filepath.Join bug with absolute dir_path.
Documents fn doctor in CLAUDE.md, .claude/rules/fn_doctor.md (rule 23),
docs/architecture.md, CHANGELOG.md (2026-05-07), and diary entry.

First fn doctor uses-functions run found drift in 7/12 apps (deuda
para sincronizar app.md con imports reales).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 01:42:10 +02:00

83 lines
2.1 KiB
Go

package infra
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
var validParseModes = map[string]bool{
"": true,
"Markdown": true,
"MarkdownV2": true,
"HTML": true,
}
// NotifyTelegram envía un mensaje a un chat de Telegram via Bot API.
// botToken: token del bot sin prefijo "bot". chatID: ID numérico o @channelname.
// parseMode: "" (plain), "Markdown", "MarkdownV2" o "HTML".
// Textos superiores a 4096 chars se truncan a 4093 + "...".
func NotifyTelegram(botToken string, chatID string, text string, parseMode string) error {
if !validParseModes[parseMode] {
return fmt.Errorf("notify_telegram: invalid parseMode %q (must be \"\", \"Markdown\", \"MarkdownV2\" or \"HTML\")", parseMode)
}
const maxLen = 4096
if len(text) > maxLen {
text = text[:4093] + "..."
}
payload := map[string]any{
"chat_id": chatID,
"text": text,
}
if parseMode != "" {
payload["parse_mode"] = parseMode
}
data, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("notify_telegram: marshal payload: %w", err)
}
url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", botToken)
client := &http.Client{Timeout: 10 * time.Second}
req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(data))
if err != nil {
return fmt.Errorf("notify_telegram: build request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("notify_telegram: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("notify_telegram: read response: %w", err)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("notify_telegram: telegram api: status=%d body=%s", resp.StatusCode, body)
}
var result struct {
OK bool `json:"ok"`
Description string `json:"description"`
}
if err := json.Unmarshal(body, &result); err != nil {
return fmt.Errorf("notify_telegram: parse response: %w", err)
}
if !result.OK {
return fmt.Errorf("notify_telegram: telegram: %s", result.Description)
}
return nil
}