Move monitoring PF infrastructure to Go

This commit is contained in:
Grendgi
2026-06-05 10:18:42 +03:00
parent ccfb261e7f
commit ed2a6c7f58
21 changed files with 2152 additions and 814 deletions

187
cmd/bot/main.go Normal file
View File

@@ -0,0 +1,187 @@
package main
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"os"
"os/signal"
"strconv"
"strings"
"syscall"
"time"
"monitoring-pf/internal/pf"
)
func main() {
cfg := pf.LoadConfig()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
app, err := pf.OpenApp(ctx, cfg)
if err != nil {
slog.Error("db_open_failed", "error", err)
os.Exit(1)
}
defer app.Close()
if !app.TG.Enabled() {
slog.Error("telegram_token_missing")
os.Exit(1)
}
slog.Info("monitoring_pf_go_bot_started")
var offset int64
for ctx.Err() == nil {
updates, err := app.TG.GetUpdates(ctx, offset)
if err != nil {
slog.Warn("telegram_get_updates_failed", "error", err)
time.Sleep(3 * time.Second)
continue
}
for _, update := range updates {
offset = update.UpdateID + 1
if update.Message != nil {
handleMessage(ctx, app, update.Message)
}
}
}
}
func handleMessage(ctx context.Context, app *pf.App, msg *pf.TGMessage) {
text := strings.TrimSpace(msg.Text)
if text == "" || !strings.HasPrefix(text, "/") {
return
}
command, arg := splitCommand(text)
switch command {
case "/start":
handleStart(ctx, app, msg, arg)
case "/whoami":
handleWhoami(ctx, app, msg)
case "/list":
handleList(ctx, app, msg)
case "/check":
handleCheck(ctx, app, msg)
}
}
func splitCommand(text string) (string, string) {
parts := strings.Fields(text)
if len(parts) == 0 {
return "", ""
}
command := strings.Split(parts[0], "@")[0]
arg := ""
if len(parts) > 1 {
arg = parts[1]
}
return command, arg
}
func handleStart(ctx context.Context, app *pf.App, msg *pf.TGMessage, portalUserID string) {
chatID := strconv.FormatInt(msg.Chat.ID, 10)
user := msg.From
username := ""
name := "user_" + chatID
if user != nil {
username = user.Username
name = user.FullName()
}
if portalUserID == "" {
_ = app.TG.SendMessage(ctx, chatID,
"Откройте Portal → Мониторинг PF и нажмите подключение Telegram.\n"+
"Бот должен получить команду вида:\n<code>/start ваш_код_из_Portal</code>")
return
}
emp, err := app.LinkTelegram(ctx, portalUserID, chatID, username, name)
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Не удалось подключить Telegram: "+err.Error())
return
}
_ = app.TG.SendMessage(ctx, chatID,
fmt.Sprintf("✅ Привет, <b>%s</b>! Telegram подключен к вашему аккаунту Portal.\nТеперь можно добавлять объекты мониторинга в Portal.", emp.Name))
}
func handleWhoami(ctx context.Context, app *pf.App, msg *pf.TGMessage) {
chatID := strconv.FormatInt(msg.Chat.ID, 10)
emp, err := app.EmployeeByChatID(ctx, chatID)
if errors.Is(err, sql.ErrNoRows) {
_ = app.TG.SendMessage(ctx, chatID, "Вы пока не подключены. Откройте Portal → Мониторинг PF и запустите подключение.\nchat_id: <code>"+chatID+"</code>")
return
}
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Ошибка: "+err.Error())
return
}
_ = app.TG.SendMessage(ctx, chatID, fmt.Sprintf("Вы: <b>%s</b>\nchat_id: <code>%s</code>", emp.Name, chatID))
}
func handleList(ctx context.Context, app *pf.App, msg *pf.TGMessage) {
chatID := strconv.FormatInt(msg.Chat.ID, 10)
emp, err := app.EmployeeByChatID(ctx, chatID)
if errors.Is(err, sql.ErrNoRows) {
_ = app.TG.SendMessage(ctx, chatID, "Сначала подключитесь через Portal → Мониторинг PF.")
return
}
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Ошибка: "+err.Error())
return
}
projects, err := app.ListProjects(ctx, emp.ID)
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Ошибка: "+err.Error())
return
}
if len(projects) == 0 {
_ = app.TG.SendMessage(ctx, chatID, "У вас пока нет проектов.")
return
}
lines := []string{fmt.Sprintf("<b>Ваши проекты (%d):</b>", len(projects))}
for _, p := range projects {
permit := "—"
if p.DLDPermit != nil {
permit = *p.DLDPermit
}
lines = append(lines, fmt.Sprintf("• #%d %s — <code>%s</code> (%s)", p.ID, p.Title, permit, p.DealType))
}
_ = app.TG.SendMessage(ctx, chatID, strings.Join(lines, "\n"))
}
func handleCheck(ctx context.Context, app *pf.App, msg *pf.TGMessage) {
chatID := strconv.FormatInt(msg.Chat.ID, 10)
emp, err := app.EmployeeByChatID(ctx, chatID)
if errors.Is(err, sql.ErrNoRows) {
_ = app.TG.SendMessage(ctx, chatID, "Сначала подключитесь через Portal → Мониторинг PF.")
return
}
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Ошибка: "+err.Error())
return
}
projects, err := app.ListProjects(ctx, emp.ID)
if err != nil {
_ = app.TG.SendMessage(ctx, chatID, "Ошибка: "+err.Error())
return
}
if len(projects) == 0 {
_ = app.TG.SendMessage(ctx, chatID, "У вас нет проектов.")
return
}
_ = app.TG.SendMessage(ctx, chatID, fmt.Sprintf("⏳ Запускаю проверку %d проектов…", len(projects)))
total := 0
for _, p := range projects {
changes, err := app.Worker.CheckProject(ctx, p.ID)
if err != nil {
slog.Warn("check_project_failed", "project_id", p.ID, "error", err)
continue
}
total += changes
}
_ = app.TG.SendMessage(ctx, chatID, fmt.Sprintf("✅ Готово. Изменений: %d", total))
}

61
cmd/scheduler/main.go Normal file
View File

@@ -0,0 +1,61 @@
package main
import (
"context"
"log/slog"
"os"
"os/signal"
"syscall"
"time"
"monitoring-pf/internal/pf"
)
func main() {
cfg := pf.LoadConfig()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
app, err := pf.OpenApp(ctx, cfg)
if err != nil {
slog.Error("db_open_failed", "error", err)
os.Exit(1)
}
defer app.Close()
interval := cfg.SchedulerInterval()
slog.Info("monitoring_pf_scheduler_started", "interval", interval.String())
timer := time.NewTimer(interval)
defer timer.Stop()
for {
select {
case <-ctx.Done():
slog.Info("monitoring_pf_scheduler_stopped")
return
case <-timer.C:
run(ctx, app)
timer.Reset(interval)
}
}
}
func run(ctx context.Context, app *pf.App) {
start := time.Now()
slog.Info("scheduled_scan_starting")
summary, err := app.Worker.CheckAll(ctx)
if err != nil {
slog.Error("scheduled_scan_failed", "error", err)
return
}
total := 0
for _, changes := range summary {
if changes > 0 {
total += changes
}
}
slog.Info("scheduled_scan_done", "projects", len(summary), "total_changes", total, "elapsed", time.Since(start).String())
}

50
cmd/server/main.go Normal file
View File

@@ -0,0 +1,50 @@
package main
import (
"context"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"monitoring-pf/internal/pf"
)
func main() {
cfg := pf.LoadConfig()
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
slog.SetDefault(logger)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
app, err := pf.OpenApp(ctx, cfg)
if err != nil {
slog.Error("db_open_failed", "error", err)
os.Exit(1)
}
defer app.Close()
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", cfg.WebHost, cfg.WebPort),
Handler: pf.Server{App: app},
ReadHeaderTimeout: 10 * time.Second,
}
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
slog.Info("monitoring_pf_go_server_started", "addr", server.Addr)
if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
slog.Error("server_failed", "error", err)
os.Exit(1)
}
}