feat: comprehensive module system and database management improvements

This commit introduces major enhancements to the module installation system,
database management, and configuration handling for AzerothCore deployments.

## Module System Improvements

### Module SQL Staging & Installation
- Refactor module SQL staging to properly handle AzerothCore's sql/ directory structure
- Fix SQL staging path to use correct AzerothCore format (sql/custom/db_*/*)
- Implement conditional module database importing based on enabled modules
- Add support for both cpp-modules and lua-scripts module types
- Handle rsync exit code 23 (permission warnings) gracefully during deployment

### Module Manifest & Automation
- Add automated module manifest generation via GitHub Actions workflow
- Implement Python-based module manifest updater with comprehensive validation
- Add module dependency tracking and SQL file discovery
- Support for blocked modules and module metadata management

## Database Management Enhancements

### Database Import System
- Add db-guard container for continuous database health monitoring and verification
- Implement conditional database import that skips when databases are current
- Add backup restoration and SQL staging coordination
- Support for Playerbots database (4th database) in all import operations
- Add comprehensive database health checking and status reporting

### Database Configuration
- Implement 10 new dbimport.conf settings from environment variables:
  - Database.Reconnect.Seconds/Attempts for connection reliability
  - Updates.AllowedModules for module auto-update control
  - Updates.Redundancy for data integrity checks
  - Worker/Synch thread settings for all three core databases
- Auto-apply dbimport.conf settings via auto-post-install.sh
- Add environment variable injection for db-import and db-guard containers

### Backup & Recovery
- Fix backup scheduler to prevent immediate execution on container startup
- Add backup status monitoring script with detailed reporting
- Implement backup import/export utilities
- Add database verification scripts for SQL update tracking

## User Import Directory

- Add new import/ directory for user-provided database files and configurations
- Support for custom SQL files, configuration overrides, and example templates
- Automatic import of user-provided databases and configs during initialization
- Documentation and examples for custom database imports

## Configuration & Environment

- Eliminate CLIENT_DATA_VERSION warning by adding default value syntax
- Improve CLIENT_DATA_VERSION documentation in .env.template
- Add comprehensive database import settings to .env and .env.template
- Update setup.sh to handle new configuration variables with proper defaults

## Monitoring & Debugging

- Add status dashboard with Go-based terminal UI (statusdash.go)
- Implement JSON status output (statusjson.sh) for programmatic access
- Add comprehensive database health check script
- Add repair-storage-permissions.sh utility for permission issues

## Testing & Documentation

- Add Phase 1 integration test suite for module installation verification
- Add comprehensive documentation for:
  - Database management (DATABASE_MANAGEMENT.md)
  - Module SQL analysis (AZEROTHCORE_MODULE_SQL_ANALYSIS.md)
  - Implementation mapping (IMPLEMENTATION_MAP.md)
  - SQL staging comparison and path coverage
  - Module assets and DBC file requirements
- Update SCRIPTS.md, ADVANCED.md, and troubleshooting documentation
- Update references from database-import/ to import/ directory

## Breaking Changes

- Renamed database-import/ directory to import/ for clarity
- Module SQL files now staged to AzerothCore-compatible paths
- db-guard container now required for proper database lifecycle management

## Bug Fixes

- Fix module SQL staging directory structure for AzerothCore compatibility
- Handle rsync exit code 23 gracefully during deployments
- Prevent backup from running immediately on container startup
- Correct SQL staging paths for proper module installation
This commit is contained in:
uprightbass360
2025-11-20 18:26:00 -05:00
committed by Deckard
parent 0d83f01995
commit e6231bb4a4
56 changed files with 11298 additions and 487 deletions

373
scripts/go/statusdash.go Normal file
View File

@@ -0,0 +1,373 @@
package main
import (
"encoding/json"
"fmt"
"log"
"os/exec"
"strings"
"time"
ui "github.com/gizak/termui/v3"
"github.com/gizak/termui/v3/widgets"
)
type Service struct {
Name string `json:"name"`
Label string `json:"label"`
Status string `json:"status"`
Health string `json:"health"`
StartedAt string `json:"started_at"`
Image string `json:"image"`
ExitCode string `json:"exit_code"`
}
type ContainerStats struct {
CPU float64 `json:"cpu"`
Memory string `json:"memory"`
MemoryPercent float64 `json:"memory_percent"`
}
type Port struct {
Name string `json:"name"`
Port string `json:"port"`
Reachable bool `json:"reachable"`
}
type DirInfo struct {
Path string `json:"path"`
Exists bool `json:"exists"`
Size string `json:"size"`
}
type VolumeInfo struct {
Name string `json:"name"`
Exists bool `json:"exists"`
Mountpoint string `json:"mountpoint"`
}
type UserStats struct {
Accounts int `json:"accounts"`
Online int `json:"online"`
Characters int `json:"characters"`
Active7d int `json:"active7d"`
}
type Module struct {
Name string `json:"name"`
Key string `json:"key"`
Description string `json:"description"`
Category string `json:"category"`
Type string `json:"type"`
}
type Snapshot struct {
Timestamp string `json:"timestamp"`
Project string `json:"project"`
Network string `json:"network"`
Services []Service `json:"services"`
Ports []Port `json:"ports"`
Modules []Module `json:"modules"`
Storage map[string]DirInfo `json:"storage"`
Volumes map[string]VolumeInfo `json:"volumes"`
Users UserStats `json:"users"`
Stats map[string]ContainerStats `json:"stats"`
}
func runSnapshot() (*Snapshot, error) {
cmd := exec.Command("./scripts/bash/statusjson.sh")
output, err := cmd.Output()
if err != nil {
return nil, err
}
snap := &Snapshot{}
if err := json.Unmarshal(output, snap); err != nil {
return nil, err
}
return snap, nil
}
func buildServicesTable(s *Snapshot) *TableNoCol {
table := NewTableNoCol()
rows := [][]string{{"Service", "Status", "Health", "CPU%", "Memory"}}
for _, svc := range s.Services {
cpu := "-"
mem := "-"
if stats, ok := s.Stats[svc.Name]; ok {
cpu = fmt.Sprintf("%.1f", stats.CPU)
mem = strings.Split(stats.Memory, " / ")[0] // Just show used, not total
}
// Combine health with exit code for stopped containers
health := svc.Health
if svc.Status != "running" && svc.ExitCode != "0" && svc.ExitCode != "" {
health = fmt.Sprintf("%s (%s)", svc.Health, svc.ExitCode)
}
rows = append(rows, []string{svc.Label, svc.Status, health, cpu, mem})
}
table.Rows = rows
table.RowSeparator = false
table.Border = true
table.Title = "Services"
return table
}
func buildPortsTable(s *Snapshot) *TableNoCol {
table := NewTableNoCol()
rows := [][]string{{"Port", "Number", "Reachable"}}
for _, p := range s.Ports {
state := "down"
if p.Reachable {
state = "up"
}
rows = append(rows, []string{p.Name, p.Port, state})
}
table.Rows = rows
table.RowSeparator = true
table.Border = true
table.Title = "Ports"
return table
}
func buildModulesList(s *Snapshot) *widgets.List {
list := widgets.NewList()
list.Title = fmt.Sprintf("Modules (%d)", len(s.Modules))
rows := make([]string, len(s.Modules))
for i, mod := range s.Modules {
rows[i] = mod.Name
}
list.Rows = rows
list.WrapText = false
list.Border = true
list.BorderStyle = ui.NewStyle(ui.ColorCyan)
list.SelectedRowStyle = ui.NewStyle(ui.ColorCyan)
return list
}
func buildStorageParagraph(s *Snapshot) *widgets.Paragraph {
var b strings.Builder
fmt.Fprintf(&b, "STORAGE:\n")
entries := []struct {
Key string
Label string
}{
{"storage", "Storage"},
{"local_storage", "Local Storage"},
{"client_data", "Client Data"},
{"modules", "Modules"},
{"local_modules", "Local Modules"},
}
for _, item := range entries {
info, ok := s.Storage[item.Key]
if !ok {
continue
}
mark := "○"
if info.Exists {
mark = "●"
}
fmt.Fprintf(&b, " %-15s %s %s (%s)\n", item.Label, mark, info.Path, info.Size)
}
par := widgets.NewParagraph()
par.Title = "Storage"
par.Text = b.String()
par.Border = true
par.BorderStyle = ui.NewStyle(ui.ColorYellow)
return par
}
func buildVolumesParagraph(s *Snapshot) *widgets.Paragraph {
var b strings.Builder
fmt.Fprintf(&b, "VOLUMES:\n")
entries := []struct {
Key string
Label string
}{
{"client_cache", "Client Cache"},
{"mysql_data", "MySQL Data"},
}
for _, item := range entries {
info, ok := s.Volumes[item.Key]
if !ok {
continue
}
mark := "○"
if info.Exists {
mark = "●"
}
fmt.Fprintf(&b, " %-13s %s %s\n", item.Label, mark, info.Mountpoint)
}
par := widgets.NewParagraph()
par.Title = "Volumes"
par.Text = b.String()
par.Border = true
par.BorderStyle = ui.NewStyle(ui.ColorYellow)
return par
}
func renderSnapshot(s *Snapshot, selectedModule int) (*widgets.List, *ui.Grid) {
servicesTable := buildServicesTable(s)
for i := 1; i < len(servicesTable.Rows); i++ {
if servicesTable.RowStyles == nil {
servicesTable.RowStyles = make(map[int]ui.Style)
}
state := strings.ToLower(servicesTable.Rows[i][1])
switch state {
case "running", "healthy":
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorGreen)
case "restarting", "unhealthy":
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorRed)
case "exited":
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorYellow)
default:
servicesTable.RowStyles[i] = ui.NewStyle(ui.ColorWhite)
}
}
portsTable := buildPortsTable(s)
for i := 1; i < len(portsTable.Rows); i++ {
if portsTable.RowStyles == nil {
portsTable.RowStyles = make(map[int]ui.Style)
}
if portsTable.Rows[i][2] == "up" {
portsTable.RowStyles[i] = ui.NewStyle(ui.ColorGreen)
} else {
portsTable.RowStyles[i] = ui.NewStyle(ui.ColorRed)
}
}
modulesList := buildModulesList(s)
if selectedModule >= 0 && selectedModule < len(modulesList.Rows) {
modulesList.SelectedRow = selectedModule
}
helpPar := widgets.NewParagraph()
helpPar.Title = "Controls"
helpPar.Text = " ↓ : Down\n ↑ : Up"
helpPar.Border = true
helpPar.BorderStyle = ui.NewStyle(ui.ColorMagenta)
moduleInfoPar := widgets.NewParagraph()
moduleInfoPar.Title = "Module Info"
if selectedModule >= 0 && selectedModule < len(s.Modules) {
mod := s.Modules[selectedModule]
moduleInfoPar.Text = fmt.Sprintf("%s\n\nCategory: %s\nType: %s", mod.Description, mod.Category, mod.Type)
} else {
moduleInfoPar.Text = "Select a module to view info"
}
moduleInfoPar.Border = true
moduleInfoPar.BorderStyle = ui.NewStyle(ui.ColorMagenta)
storagePar := buildStorageParagraph(s)
storagePar.Border = true
storagePar.BorderStyle = ui.NewStyle(ui.ColorYellow)
storagePar.PaddingLeft = 1
storagePar.PaddingRight = 1
volumesPar := buildVolumesParagraph(s)
header := widgets.NewParagraph()
header.Text = fmt.Sprintf("Project: %s\nNetwork: %s\nUpdated: %s", s.Project, s.Network, s.Timestamp)
header.Border = true
usersPar := widgets.NewParagraph()
usersPar.Text = fmt.Sprintf("USERS:\n Accounts: %d\n Online: %d\n Characters: %d\n Active 7d: %d", s.Users.Accounts, s.Users.Online, s.Users.Characters, s.Users.Active7d)
usersPar.Border = true
grid := ui.NewGrid()
termWidth, termHeight := ui.TerminalDimensions()
grid.SetRect(0, 0, termWidth, termHeight)
grid.Set(
ui.NewRow(0.18,
ui.NewCol(0.6, header),
ui.NewCol(0.4, usersPar),
),
ui.NewRow(0.42,
ui.NewCol(0.6, servicesTable),
ui.NewCol(0.4, portsTable),
),
ui.NewRow(0.40,
ui.NewCol(0.25, modulesList),
ui.NewCol(0.15,
ui.NewRow(0.30, helpPar),
ui.NewRow(0.70, moduleInfoPar),
),
ui.NewCol(0.6,
ui.NewRow(0.55,
ui.NewCol(1.0, storagePar),
),
ui.NewRow(0.45,
ui.NewCol(1.0, volumesPar),
),
),
),
)
ui.Render(grid)
return modulesList, grid
}
func main() {
if err := ui.Init(); err != nil {
log.Fatalf("failed to init termui: %v", err)
}
defer ui.Close()
snapshot, err := runSnapshot()
if err != nil {
log.Fatalf("failed to fetch snapshot: %v", err)
}
selectedModule := 0
modulesWidget, currentGrid := renderSnapshot(snapshot, selectedModule)
snapCh := make(chan *Snapshot, 1)
go func() {
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for range ticker.C {
snap, err := runSnapshot()
if err != nil {
log.Printf("snapshot error: %v", err)
continue
}
select {
case snapCh <- snap:
default:
}
}
}()
events := ui.PollEvents()
for {
select {
case e := <-events:
switch e.ID {
case "q", "<C-c>":
return
case "<Down>", "j":
if selectedModule < len(snapshot.Modules)-1 {
selectedModule++
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
}
case "<Up>", "k":
if selectedModule > 0 {
selectedModule--
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
}
case "<Resize>":
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
continue
}
if modulesWidget != nil {
if selectedModule >= 0 && selectedModule < len(modulesWidget.Rows) {
modulesWidget.SelectedRow = selectedModule
}
}
if currentGrid != nil {
ui.Render(currentGrid)
}
case snap := <-snapCh:
snapshot = snap
if selectedModule >= len(snapshot.Modules) {
selectedModule = len(snapshot.Modules) - 1
if selectedModule < 0 {
selectedModule = 0
}
}
modulesWidget, currentGrid = renderSnapshot(snapshot, selectedModule)
}
}
}