feat: complete 2.0.7.12 platform overhaul
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"ymhut-box/server/unified-management/internal/config"
|
||||
)
|
||||
|
||||
func TestFeedbackSummaryPageOmitsHeavyDetailFields(t *testing.T) {
|
||||
store := openAdminPerformanceStore(t)
|
||||
defer store.Close()
|
||||
item := Feedback{Code: "FB-SUMMARY-1", Title: "Summary", Body: "large private body", SummaryText: "short", Status: "new"}
|
||||
if err := store.InsertFeedback(item); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
items, total, err := store.ListFeedbackSummariesFiltered(1, 20, FeedbackFilters{})
|
||||
if err != nil || total != 1 || len(items) != 1 {
|
||||
t.Fatalf("summary list total=%d items=%d err=%v", total, len(items), err)
|
||||
}
|
||||
if items[0].Body != "" || items[0].SummaryText != "short" {
|
||||
t.Fatalf("summary list loaded heavy fields: %#v", items[0])
|
||||
}
|
||||
detail, err := store.GetFeedback("FB-SUMMARY-1")
|
||||
if err != nil || detail.Body != "large private body" {
|
||||
t.Fatalf("detail body=%q err=%v", detail.Body, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSystemLogsUseFilteredDatabasePagination(t *testing.T) {
|
||||
store := openAdminPerformanceStore(t)
|
||||
defer store.Close()
|
||||
if err := store.InsertAudit(AuditLog{Actor: "admin", Type: "release.saved", Target: "2.0", Message: "published"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.RecordSourceCall(SourceCall{SourceID: "music", Status: "ok", LatencyMS: 30, Client: "test"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
page, err := store.ListSystemLogsPage(SystemLogFilters{Page: 1, PerPage: 1})
|
||||
if err != nil || page.Total < 2 || len(page.Items) != 1 {
|
||||
t.Fatalf("system log page=%#v err=%v", page, err)
|
||||
}
|
||||
filtered, err := store.ListSystemLogsPage(SystemLogFilters{Page: 1, PerPage: 10, Category: "client", Query: "music"})
|
||||
if err != nil || filtered.Total != 1 || filtered.Items[0].Category != "client" {
|
||||
t.Fatalf("filtered system logs=%#v err=%v", filtered, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardWindowIncludesSummaryAndGeneratedMetadata(t *testing.T) {
|
||||
store := openAdminPerformanceStore(t)
|
||||
defer store.Close()
|
||||
source, err := store.UpsertSource(Source{CategoryID: "network", CategoryName: "网络", SourceID: "status", Name: "状态", APIURL: "https://example.com", Enabled: true, EnabledSet: true, ClientVisible: true, ClientVisibleSet: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := store.RecordSourceCheck(source.ID, "ok", 42, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
overview, err := store.DashboardOverviewWindow(60, time.Now().UTC().Add(-time.Hour).Format(time.RFC3339))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if overview["generatedAt"] == "" || len(overview["sourceRows"].([]map[string]any)) != 1 {
|
||||
t.Fatalf("dashboard metadata missing: %#v", overview)
|
||||
}
|
||||
}
|
||||
|
||||
func openAdminPerformanceStore(t *testing.T) *Store {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
store, err := Open(&config.Config{
|
||||
StorageDir: root,
|
||||
Database: config.DatabaseConfig{Provider: "sqlite", SQLitePath: filepath.Join(root, "admin-performance.sqlite"), HealthIntervalSec: 3600, MaxOpenConns: 2, MaxIdleConns: 1},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return store
|
||||
}
|
||||
@@ -8,21 +8,62 @@ import (
|
||||
)
|
||||
|
||||
func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
return s.DashboardOverviewWindow(limit, "")
|
||||
}
|
||||
|
||||
func (s *Store) DashboardOverviewWindow(limit int, since string) (map[string]any, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 80
|
||||
}
|
||||
feedbackTotal, _ := s.countTable("feedback_tickets")
|
||||
feedbackToday, _ := s.countWhere("feedback_tickets", "created_at LIKE ?", time.Now().UTC().Format("2006-01-02")+"%")
|
||||
sourceTotal, _ := s.countTable("source_endpoints")
|
||||
sourceVisible, _ := s.countWhere("source_endpoints", "enabled = 1 AND client_visible = 1")
|
||||
releaseTotal, _ := s.countTable("release_notices")
|
||||
mailFailed, _ := s.countWhere("mail_records", "status = ?", "failed")
|
||||
statusCounts, _ := s.groupCounts("feedback_tickets", "status")
|
||||
healthCounts, _ := s.groupCounts("source_endpoints", "last_status")
|
||||
recentChecks, _ := s.RecentSourceChecks(limit)
|
||||
recentCalls, _ := s.RecentSourceCalls(limit)
|
||||
averageLatency, _ := s.AverageSourceLatencyBuckets(limit)
|
||||
audit, _ := s.ListAuditLogs(10)
|
||||
var feedbackTotal, feedbackToday, sourceTotal, sourceVisible, releaseTotal, mailFailed int
|
||||
today := time.Now().UTC().Format("2006-01-02") + "T00:00:00Z"
|
||||
if err := s.queryRow(`SELECT
|
||||
(SELECT COUNT(*) FROM feedback_tickets),
|
||||
(SELECT COUNT(*) FROM feedback_tickets WHERE created_at >= ?),
|
||||
(SELECT COUNT(*) FROM source_endpoints),
|
||||
(SELECT COUNT(*) FROM source_endpoints WHERE enabled = 1 AND client_visible = 1),
|
||||
(SELECT COUNT(*) FROM release_notices),
|
||||
(SELECT COUNT(*) FROM mail_records WHERE status = 'failed')`, today).Scan(
|
||||
&feedbackTotal, &feedbackToday, &sourceTotal, &sourceVisible, &releaseTotal, &mailFailed,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
warnings := []string{}
|
||||
statusCounts, err := s.groupCounts("feedback_tickets", "status")
|
||||
if err != nil {
|
||||
warnings = append(warnings, "feedback status unavailable: "+err.Error())
|
||||
statusCounts = map[string]int{}
|
||||
}
|
||||
healthCounts, err := s.groupCounts("source_endpoints", "last_status")
|
||||
if err != nil {
|
||||
warnings = append(warnings, "source health unavailable: "+err.Error())
|
||||
healthCounts = map[string]int{}
|
||||
}
|
||||
recentChecks, err := s.RecentSourceChecksWindow(limit, since)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "source checks unavailable: "+err.Error())
|
||||
recentChecks = []map[string]any{}
|
||||
}
|
||||
recentCalls, err := s.RecentSourceCallsWindow(limit, since)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "client calls unavailable: "+err.Error())
|
||||
recentCalls = []map[string]any{}
|
||||
}
|
||||
averageLatency, err := s.AverageSourceLatencyBucketsWindow(limit, since)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "latency trend unavailable: "+err.Error())
|
||||
averageLatency = []map[string]any{}
|
||||
}
|
||||
audit, err := s.ListAuditLogs(10)
|
||||
if err != nil {
|
||||
warnings = append(warnings, "audit summary unavailable: "+err.Error())
|
||||
audit = []AuditLog{}
|
||||
}
|
||||
sourceRows, err := s.DashboardSourceRows()
|
||||
if err != nil {
|
||||
warnings = append(warnings, "source summary unavailable: "+err.Error())
|
||||
sourceRows = []map[string]any{}
|
||||
}
|
||||
return map[string]any{
|
||||
"ok": true,
|
||||
"kpis": map[string]any{
|
||||
@@ -40,14 +81,28 @@ func (s *Store) DashboardOverview(limit int) (map[string]any, error) {
|
||||
"clientCalls": recentCalls,
|
||||
"database": s.Status(),
|
||||
"audit": audit,
|
||||
"sourceRows": sourceRows,
|
||||
"generatedAt": time.Now().UTC().Format(time.RFC3339),
|
||||
"warnings": warnings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) AverageSourceLatencyBuckets(limit int) ([]map[string]any, error) {
|
||||
return s.AverageSourceLatencyBucketsWindow(limit, "")
|
||||
}
|
||||
|
||||
func (s *Store) AverageSourceLatencyBucketsWindow(limit int, since string) ([]map[string]any, error) {
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 80
|
||||
}
|
||||
rows, err := s.query(`SELECT checked_at, latency_ms, status FROM endpoint_health_checks ORDER BY checked_at DESC, id DESC LIMIT ?`, limit*4)
|
||||
where := ""
|
||||
args := []any{}
|
||||
if strings.TrimSpace(since) != "" {
|
||||
where = " WHERE checked_at >= ?"
|
||||
args = append(args, since)
|
||||
}
|
||||
args = append(args, limit*4)
|
||||
rows, err := s.query(`SELECT checked_at, latency_ms, status FROM endpoint_health_checks`+where+` ORDER BY checked_at DESC, id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,9 +180,20 @@ func latencyBucketLabel(value string) string {
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceChecks(limit int) ([]map[string]any, error) {
|
||||
return s.RecentSourceChecksWindow(limit, "")
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceChecksWindow(limit int, since string) ([]map[string]any, error) {
|
||||
where := ""
|
||||
args := []any{}
|
||||
if strings.TrimSpace(since) != "" {
|
||||
where = " WHERE h.checked_at >= ?"
|
||||
args = append(args, since)
|
||||
}
|
||||
args = append(args, limit)
|
||||
rows, err := s.query(`SELECT h.id, h.source_db_id, COALESCE(e.source_id, ''), COALESCE(e.name, ''), h.status, h.latency_ms, h.error, h.checked_at
|
||||
FROM endpoint_health_checks h LEFT JOIN source_endpoints e ON e.id = h.source_db_id
|
||||
ORDER BY h.checked_at DESC, h.id DESC LIMIT ?`, limit)
|
||||
`+where+` ORDER BY h.checked_at DESC, h.id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -152,7 +218,18 @@ func (s *Store) RecentSourceChecks(limit int) ([]map[string]any, error) {
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceCalls(limit int) ([]map[string]any, error) {
|
||||
rows, err := s.query(`SELECT id, source_id, status, latency_ms, error, client, created_at FROM endpoint_call_logs ORDER BY created_at DESC, id DESC LIMIT ?`, limit)
|
||||
return s.RecentSourceCallsWindow(limit, "")
|
||||
}
|
||||
|
||||
func (s *Store) RecentSourceCallsWindow(limit int, since string) ([]map[string]any, error) {
|
||||
where := ""
|
||||
args := []any{}
|
||||
if strings.TrimSpace(since) != "" {
|
||||
where = " WHERE created_at >= ?"
|
||||
args = append(args, since)
|
||||
}
|
||||
args = append(args, limit)
|
||||
rows, err := s.query(`SELECT id, source_id, status, latency_ms, error, client, created_at FROM endpoint_call_logs`+where+` ORDER BY created_at DESC, id DESC LIMIT ?`, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -170,6 +247,30 @@ func (s *Store) RecentSourceCalls(limit int) ([]map[string]any, error) {
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) DashboardSourceRows() ([]map[string]any, error) {
|
||||
rows, err := s.query(`SELECT source_id, category_id, category_name, name, enabled, client_visible,
|
||||
last_status, last_latency_ms, last_checked_at, last_error, consecutive_failure
|
||||
FROM source_endpoints ORDER BY category_id ASC, name ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []map[string]any{}
|
||||
for rows.Next() {
|
||||
var sourceID, categoryID, categoryName, name, status, checkedAt, lastError string
|
||||
var enabled, visible, latency, failures int
|
||||
if err := rows.Scan(&sourceID, &categoryID, &categoryName, &name, &enabled, &visible, &status, &latency, &checkedAt, &lastError, &failures); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, map[string]any{
|
||||
"sourceId": sourceID, "categoryId": categoryID, "categoryName": categoryName, "name": name,
|
||||
"enabled": enabled == 1, "clientVisible": visible == 1, "status": status,
|
||||
"latencyMs": latency, "checkedAt": checkedAt, "healthError": lastError, "consecutiveFailure": failures,
|
||||
})
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) InsertAudit(log AuditLog) error {
|
||||
return s.InsertAuditContext(context.Background(), log)
|
||||
}
|
||||
|
||||
@@ -106,6 +106,14 @@ func (s *Store) ListFeedbacks(limit int) ([]Feedback, error) {
|
||||
}
|
||||
|
||||
func (s *Store) ListFeedbacksFiltered(page, perPage int, filters FeedbackFilters) ([]Feedback, int, error) {
|
||||
return s.listFeedbacksFiltered(page, perPage, filters, false)
|
||||
}
|
||||
|
||||
func (s *Store) ListFeedbackSummariesFiltered(page, perPage int, filters FeedbackFilters) ([]Feedback, int, error) {
|
||||
return s.listFeedbacksFiltered(page, perPage, filters, true)
|
||||
}
|
||||
|
||||
func (s *Store) listFeedbacksFiltered(page, perPage int, filters FeedbackFilters, summaryOnly bool) ([]Feedback, int, error) {
|
||||
page, perPage = normalizePage(page, perPage)
|
||||
where, args := feedbackWhere(filters)
|
||||
var total int
|
||||
@@ -117,12 +125,21 @@ func (s *Store) ListFeedbacksFiltered(page, perPage int, filters FeedbackFilters
|
||||
order = ` ORDER BY created_at ASC`
|
||||
}
|
||||
args = append(args, perPage, (page-1)*perPage)
|
||||
rows, err := s.query(feedbackSelectSQL()+where+order+` LIMIT ? OFFSET ?`, args...)
|
||||
selectSQL := feedbackSelectSQL()
|
||||
if summaryOnly {
|
||||
selectSQL = feedbackSummarySelectSQL()
|
||||
}
|
||||
rows, err := s.query(selectSQL+where+order+` LIMIT ? OFFSET ?`, args...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items, err := scanFeedbackRows(rows)
|
||||
var items []Feedback
|
||||
if summaryOnly {
|
||||
items, err = scanFeedbackSummaryRows(rows)
|
||||
} else {
|
||||
items, err = scanFeedbackRows(rows)
|
||||
}
|
||||
return items, total, err
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,87 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Store) UpsertReleasePackage(item ReleasePackage) (ReleasePackage, error) {
|
||||
item.FileName = strings.TrimSpace(item.FileName)
|
||||
if item.FileName == "" {
|
||||
return ReleasePackage{}, errors.New("file name is required")
|
||||
}
|
||||
if item.CreatedAt == "" {
|
||||
if existing, ok, err := s.GetReleasePackage(item.FileName); err == nil && ok {
|
||||
item.CreatedAt = existing.CreatedAt
|
||||
} else if err != nil {
|
||||
return ReleasePackage{}, err
|
||||
} else {
|
||||
item.CreatedAt = Now()
|
||||
}
|
||||
}
|
||||
if item.UpdatedAt == "" {
|
||||
item.UpdatedAt = Now()
|
||||
}
|
||||
columns := []string{"product", "version", "platform", "arch", "file_name", "url", "sha256", "size_bytes", "enabled", "created_at", "updated_at"}
|
||||
conn, d := s.active()
|
||||
_, err := conn.Exec(d.rebind(d.upsert("release_packages", columns, []string{"file_name"})),
|
||||
sanitize(item.Product), sanitize(item.Version), sanitize(item.Platform), sanitize(item.Arch), sanitize(item.FileName),
|
||||
sanitizeLong(item.URL, 1200), sanitize(item.SHA256), item.SizeBytes, d.boolExpr(item.Enabled), item.CreatedAt, item.UpdatedAt)
|
||||
if err != nil {
|
||||
s.markFailover(err)
|
||||
return ReleasePackage{}, err
|
||||
}
|
||||
stored, ok, err := s.GetReleasePackage(item.FileName)
|
||||
if err != nil {
|
||||
return ReleasePackage{}, err
|
||||
}
|
||||
if !ok {
|
||||
return ReleasePackage{}, errors.New("release package was not persisted")
|
||||
}
|
||||
return stored, nil
|
||||
}
|
||||
|
||||
func (s *Store) GetReleasePackage(fileName string) (ReleasePackage, bool, error) {
|
||||
var item ReleasePackage
|
||||
var enabled int
|
||||
err := s.queryRow(releasePackageSelectSQL()+` WHERE file_name = ?`, strings.TrimSpace(fileName)).Scan(
|
||||
&item.ID, &item.Product, &item.Version, &item.Platform, &item.Arch, &item.FileName, &item.URL,
|
||||
&item.SHA256, &item.SizeBytes, &enabled, &item.CreatedAt, &item.UpdatedAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return ReleasePackage{}, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
return ReleasePackage{}, false, err
|
||||
}
|
||||
item.Enabled = enabled != 0
|
||||
return item, true, nil
|
||||
}
|
||||
|
||||
func (s *Store) ListReleasePackages() ([]ReleasePackage, error) {
|
||||
rows, err := s.query(releasePackageSelectSQL() + ` ORDER BY file_name ASC`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []ReleasePackage{}
|
||||
for rows.Next() {
|
||||
var item ReleasePackage
|
||||
var enabled int
|
||||
if err := rows.Scan(&item.ID, &item.Product, &item.Version, &item.Platform, &item.Arch, &item.FileName, &item.URL,
|
||||
&item.SHA256, &item.SizeBytes, &enabled, &item.CreatedAt, &item.UpdatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Enabled = enabled != 0
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) DeleteReleasePackage(fileName string) error {
|
||||
_, err := s.exec(`DELETE FROM release_packages WHERE file_name = ?`, strings.TrimSpace(fileName))
|
||||
return err
|
||||
}
|
||||
|
||||
func releasePackageSelectSQL() string {
|
||||
return `SELECT id, product, version, platform, arch, file_name, url, sha256, size_bytes, enabled, created_at, updated_at FROM release_packages`
|
||||
}
|
||||
|
||||
func (s *Store) UpsertReleaseNotice(item ReleaseNotice) (ReleaseNotice, error) {
|
||||
now := Now()
|
||||
item.Version = strings.TrimSpace(item.Version)
|
||||
|
||||
@@ -21,6 +21,11 @@ func feedbackSelectSQL() string {
|
||||
mail_sent, remote_addr, tags, created_at, updated_at, last_activity_at FROM feedback_tickets`
|
||||
}
|
||||
|
||||
func feedbackSummarySelectSQL() string {
|
||||
return `SELECT code, title, type, severity, category, priority, status, assignee, source_channel,
|
||||
risk_score, summary_text, mail_sent, tags, created_at, updated_at, last_activity_at FROM feedback_tickets`
|
||||
}
|
||||
|
||||
func releaseNoticeSelectSQL() string {
|
||||
return `SELECT id, version, build, channel, title, message, release_notes, message_md, release_notes_md,
|
||||
download_url, notice_file, raw_json, published_at, created_at, updated_at FROM release_notices`
|
||||
@@ -65,6 +70,24 @@ func scanFeedbackRows(rows *sql.Rows) ([]Feedback, error) {
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func scanFeedbackSummaryRows(rows *sql.Rows) ([]Feedback, error) {
|
||||
items := []Feedback{}
|
||||
for rows.Next() {
|
||||
var item Feedback
|
||||
var mailSent int
|
||||
var tags string
|
||||
if err := rows.Scan(&item.Code, &item.Title, &item.Type, &item.Severity, &item.Category, &item.Priority,
|
||||
&item.Status, &item.Assignee, &item.SourceChannel, &item.RiskScore, &item.SummaryText, &mailSent,
|
||||
&tags, &item.CreatedAt, &item.UpdatedAt, &item.LastActivityAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.MailSent = mailSent == 1
|
||||
_ = json.Unmarshal([]byte(tags), &item.Tags)
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func sourceSelectSQL() string {
|
||||
return `SELECT id, category_id, category_name, source_id, name, description, method, api_url, url_template, thumbnail_url,
|
||||
proxy_mode, timeout_ms, retry_count, cache_seconds, check_interval_sec, enabled, client_visible, supported_formats,
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const CurrentSchemaVersion = "2026-06-compat-baseline"
|
||||
const CurrentSchemaVersion = "2026-08-admin-performance-indexes"
|
||||
|
||||
func (s *Store) migrate(conn *sql.DB, d dialect) error {
|
||||
statements := []string{}
|
||||
@@ -296,11 +296,20 @@ type schemaIndex struct {
|
||||
func schemaIndexes() []schemaIndex {
|
||||
return []schemaIndex{
|
||||
{name: "idx_feedback_tickets_activity", table: "feedback_tickets", columns: "last_activity_at"},
|
||||
{name: "idx_feedback_tickets_status_activity", table: "feedback_tickets", columns: "status, last_activity_at"},
|
||||
{name: "idx_feedback_tickets_priority_activity", table: "feedback_tickets", columns: "priority, last_activity_at"},
|
||||
{name: "idx_feedback_tickets_category_activity", table: "feedback_tickets", columns: "category, last_activity_at"},
|
||||
{name: "idx_feedback_comments_code", table: "feedback_comments", columns: "feedback_code"},
|
||||
{name: "idx_feedback_attachments_code", table: "feedback_attachments", columns: "feedback_code"},
|
||||
{name: "idx_feedback_events_code", table: "feedback_events", columns: "feedback_code"},
|
||||
{name: "idx_mail_records_code", table: "mail_records", columns: "feedback_code"},
|
||||
{name: "idx_mail_records_status_created", table: "mail_records", columns: "status, created_at"},
|
||||
{name: "idx_source_endpoints_checked", table: "source_endpoints", columns: "last_checked_at"},
|
||||
{name: "idx_endpoint_health_checked", table: "endpoint_health_checks", columns: "checked_at, id"},
|
||||
{name: "idx_endpoint_health_source_checked", table: "endpoint_health_checks", columns: "source_db_id, checked_at"},
|
||||
{name: "idx_endpoint_call_logs_source", table: "endpoint_call_logs", columns: "source_id"},
|
||||
{name: "idx_endpoint_call_logs_source_created", table: "endpoint_call_logs", columns: "source_id, created_at"},
|
||||
{name: "idx_endpoint_call_logs_created", table: "endpoint_call_logs", columns: "created_at, id"},
|
||||
{name: "idx_audit_logs_created", table: "audit_logs", columns: "created_at"},
|
||||
{name: "idx_audit_logs_type", table: "audit_logs", columns: "type"},
|
||||
{name: "idx_audit_logs_target", table: "audit_logs", columns: "target"},
|
||||
@@ -350,7 +359,7 @@ func (s *Store) recordSchemaVersion(conn *sql.DB, d dialect) error {
|
||||
_, err := conn.Exec(d.rebind(d.upsert("schema_migrations", columns, []string{"version"})),
|
||||
CurrentSchemaVersion,
|
||||
Now(),
|
||||
"unified-management layered monolith baseline",
|
||||
"admin performance indexes and production asset diagnostics",
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
import "strings"
|
||||
|
||||
func (s *Store) ListSystemLogsPage(filters SystemLogFilters) (SystemLogPage, error) {
|
||||
page := filters.Page
|
||||
@@ -19,209 +14,71 @@ func (s *Store) ListSystemLogsPage(filters SystemLogFilters) (SystemLogPage, err
|
||||
if perPage > 100 {
|
||||
perPage = 100
|
||||
}
|
||||
items, err := s.collectSystemLogs(filters)
|
||||
base := systemLogUnionSQL()
|
||||
where, args := systemLogWhere(filters)
|
||||
var total int
|
||||
if err := s.queryRow(`SELECT COUNT(*) FROM (`+base+`) system_logs`+where, args...).Scan(&total); err != nil {
|
||||
return SystemLogPage{}, err
|
||||
}
|
||||
queryArgs := append(append([]any{}, args...), perPage, (page-1)*perPage)
|
||||
rows, err := s.query(`SELECT id, category, type, target, status, message, detail, created_at
|
||||
FROM (`+base+`) system_logs`+where+` ORDER BY created_at DESC, id DESC LIMIT ? OFFSET ?`, queryArgs...)
|
||||
if err != nil {
|
||||
return SystemLogPage{}, err
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].CreatedAt == items[j].CreatedAt {
|
||||
return items[i].ID > items[j].ID
|
||||
}
|
||||
return items[i].CreatedAt > items[j].CreatedAt
|
||||
})
|
||||
total := len(items)
|
||||
start := (page - 1) * perPage
|
||||
if start > total {
|
||||
start = total
|
||||
}
|
||||
end := start + perPage
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return SystemLogPage{Items: items[start:end], Total: total, Page: page, PerPage: perPage}, nil
|
||||
}
|
||||
|
||||
func (s *Store) collectSystemLogs(filters SystemLogFilters) ([]SystemLogItem, error) {
|
||||
items := []SystemLogItem{}
|
||||
appendItems := func(category string, next []SystemLogItem, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range next {
|
||||
if matchesSystemLog(filters, item, category) {
|
||||
items = append(items, item)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
logs, err := s.operationLogs()
|
||||
if err := appendItems("operation", logs, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs, err = s.healthLogs()
|
||||
if err := appendItems("health", logs, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs, err = s.clientCallLogs()
|
||||
if err := appendItems("client", logs, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs, err = s.databaseSyncLogs()
|
||||
if err := appendItems("database_sync", logs, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
logs, err = s.legacySyncLogs()
|
||||
if err := appendItems("legacy_sync", logs, err); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *Store) operationLogs() ([]SystemLogItem, error) {
|
||||
rows, err := s.query(`SELECT id, actor, type, target, message, ip, user_agent, created_at FROM audit_logs ORDER BY id DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemLogItem{}
|
||||
for rows.Next() {
|
||||
var item AuditLog
|
||||
if err := rows.Scan(&item.ID, &item.Actor, &item.Type, &item.Target, &item.Message, &item.IP, &item.UserAgent, &item.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
var item SystemLogItem
|
||||
if err := rows.Scan(&item.ID, &item.Category, &item.Type, &item.Target, &item.Status, &item.Message, &item.Detail, &item.CreatedAt); err != nil {
|
||||
return SystemLogPage{}, err
|
||||
}
|
||||
items = append(items, SystemLogItem{
|
||||
ID: item.ID,
|
||||
Category: "operation",
|
||||
Type: item.Type,
|
||||
Target: item.Target,
|
||||
Status: firstNonEmpty(item.Actor, "system"),
|
||||
Message: item.Message,
|
||||
Detail: strings.TrimSpace(item.IP + " " + item.UserAgent),
|
||||
CreatedAt: item.CreatedAt,
|
||||
})
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, rows.Err()
|
||||
if err := rows.Err(); err != nil {
|
||||
return SystemLogPage{}, err
|
||||
}
|
||||
return SystemLogPage{Items: items, Total: total, Page: page, PerPage: perPage}, nil
|
||||
}
|
||||
|
||||
func (s *Store) healthLogs() ([]SystemLogItem, error) {
|
||||
rows, err := s.query(`SELECT h.id, h.source_db_id, COALESCE(e.source_id, ''), COALESCE(e.name, ''), h.status, h.latency_ms, h.error, h.checked_at
|
||||
func systemLogUnionSQL() string {
|
||||
return `SELECT id, 'operation' AS category, type, target,
|
||||
COALESCE(NULLIF(actor, ''), 'system') AS status, message, user_agent AS detail, created_at
|
||||
FROM audit_logs
|
||||
UNION ALL
|
||||
SELECT h.id, 'health' AS category, 'endpoint.health' AS type,
|
||||
COALESCE(NULLIF(e.source_id, ''), 'unknown') AS target, h.status,
|
||||
COALESCE(NULLIF(e.name, ''), 'unknown endpoint') AS message, h.error AS detail, h.checked_at AS created_at
|
||||
FROM endpoint_health_checks h LEFT JOIN source_endpoints e ON e.id = h.source_db_id
|
||||
ORDER BY h.id DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemLogItem{}
|
||||
for rows.Next() {
|
||||
var id, sourceDBID int64
|
||||
var sourceID, name, status, message, checkedAt string
|
||||
var latency int
|
||||
if err := rows.Scan(&id, &sourceDBID, &sourceID, &name, &status, &latency, &message, &checkedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target := firstNonEmpty(sourceID, fmt.Sprintf("source#%d", sourceDBID))
|
||||
items = append(items, SystemLogItem{
|
||||
ID: id,
|
||||
Category: "health",
|
||||
Type: "endpoint.health",
|
||||
Target: target,
|
||||
Status: status,
|
||||
Message: firstNonEmpty(name, target),
|
||||
Detail: fmt.Sprintf("%dms %s", latency, message),
|
||||
CreatedAt: checkedAt,
|
||||
})
|
||||
}
|
||||
return items, rows.Err()
|
||||
UNION ALL
|
||||
SELECT id, 'client' AS category, 'endpoint.call' AS type, source_id AS target,
|
||||
status, error AS message, client AS detail, created_at
|
||||
FROM endpoint_call_logs
|
||||
UNION ALL
|
||||
SELECT id, 'database_sync' AS category, direction AS type, direction AS target,
|
||||
status, message, tables_json AS detail, COALESCE(NULLIF(finished_at, ''), started_at) AS created_at
|
||||
FROM database_sync_jobs
|
||||
UNION ALL
|
||||
SELECT id, 'legacy_sync' AS category, 'legacy.sync' AS type, 'legacy' AS target,
|
||||
status, summary AS message, stats_json AS detail, COALESCE(NULLIF(finished_at, ''), started_at) AS created_at
|
||||
FROM legacy_sync_jobs`
|
||||
}
|
||||
|
||||
func (s *Store) clientCallLogs() ([]SystemLogItem, error) {
|
||||
rows, err := s.query(`SELECT id, source_id, status, latency_ms, error, client, created_at FROM endpoint_call_logs ORDER BY id DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
func systemLogWhere(filters SystemLogFilters) (string, []any) {
|
||||
clauses := []string{}
|
||||
args := []any{}
|
||||
if category := strings.TrimSpace(filters.Category); category != "" {
|
||||
clauses = append(clauses, "category = ?")
|
||||
args = append(args, sanitize(category))
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemLogItem{}
|
||||
for rows.Next() {
|
||||
var id int64
|
||||
var sourceID, status, message, client, createdAt string
|
||||
var latency int
|
||||
if err := rows.Scan(&id, &sourceID, &status, &latency, &message, &client, &createdAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, SystemLogItem{
|
||||
ID: id,
|
||||
Category: "client",
|
||||
Type: "endpoint.call",
|
||||
Target: sourceID,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Detail: fmt.Sprintf("%dms %s", latency, client),
|
||||
CreatedAt: createdAt,
|
||||
})
|
||||
if query := strings.TrimSpace(filters.Query); query != "" {
|
||||
like := "%" + strings.ToLower(sanitize(query)) + "%"
|
||||
clauses = append(clauses, `(LOWER(category) LIKE ? OR LOWER(type) LIKE ? OR LOWER(target) LIKE ? OR
|
||||
LOWER(status) LIKE ? OR LOWER(message) LIKE ? OR LOWER(detail) LIKE ? OR LOWER(created_at) LIKE ?)`)
|
||||
args = append(args, like, like, like, like, like, like, like)
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) databaseSyncLogs() ([]SystemLogItem, error) {
|
||||
rows, err := s.query(`SELECT id, direction, status, message, tables_json, started_at, finished_at FROM database_sync_jobs ORDER BY id DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemLogItem{}
|
||||
for rows.Next() {
|
||||
job, err := scanDatabaseSyncJob(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
detail, _ := json.Marshal(map[string]any{"tables": job.Tables, "warnings": job.Warnings, "errors": job.Errors})
|
||||
items = append(items, SystemLogItem{
|
||||
ID: job.ID,
|
||||
Category: "database_sync",
|
||||
Type: job.Direction,
|
||||
Target: directionLabel(job.Direction),
|
||||
Status: job.Status,
|
||||
Message: strings.Join(job.Output, "\n"),
|
||||
Detail: string(detail),
|
||||
CreatedAt: firstNonEmpty(job.FinishedAt, job.StartedAt),
|
||||
})
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) legacySyncLogs() ([]SystemLogItem, error) {
|
||||
rows, err := s.query(`SELECT id, status, summary, stats_json, started_at, finished_at FROM legacy_sync_jobs ORDER BY id DESC LIMIT 500`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemLogItem{}
|
||||
for rows.Next() {
|
||||
var item LegacySyncJob
|
||||
if err := rows.Scan(&item.ID, &item.Status, &item.Summary, &item.StatsJSON, &item.StartedAt, &item.FinishedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, SystemLogItem{
|
||||
ID: item.ID,
|
||||
Category: "legacy_sync",
|
||||
Type: "legacy.sync",
|
||||
Target: "legacy",
|
||||
Status: item.Status,
|
||||
Message: item.Summary,
|
||||
Detail: item.StatsJSON,
|
||||
CreatedAt: firstNonEmpty(item.FinishedAt, item.StartedAt),
|
||||
})
|
||||
}
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
func matchesSystemLog(filters SystemLogFilters, item SystemLogItem, category string) bool {
|
||||
if value := strings.TrimSpace(filters.Category); value != "" && value != category && value != item.Category {
|
||||
return false
|
||||
}
|
||||
if value := strings.ToLower(strings.TrimSpace(filters.Query)); value != "" {
|
||||
haystack := strings.ToLower(strings.Join([]string{item.Category, item.Type, item.Target, item.Status, item.Message, item.Detail, item.CreatedAt}, " "))
|
||||
return strings.Contains(haystack, value)
|
||||
}
|
||||
return true
|
||||
if len(clauses) == 0 {
|
||||
return "", args
|
||||
}
|
||||
return " WHERE " + strings.Join(clauses, " AND "), args
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user