From d38c7557e0cd376a895e9e1854499ac593610282 Mon Sep 17 00:00:00 2001 From: uprightbass360 Date: Wed, 26 Nov 2025 01:31:00 -0500 Subject: [PATCH] status info --- scripts/bash/statusjson.sh | 196 +++++++++++++++++++++++++++++++++++++ scripts/go/statusdash.go | 195 ++++++++++++++++++++++++++++++++---- 2 files changed, 370 insertions(+), 21 deletions(-) diff --git a/scripts/bash/statusjson.sh b/scripts/bash/statusjson.sh index 0149f93..f6d2b13 100755 --- a/scripts/bash/statusjson.sh +++ b/scripts/bash/statusjson.sh @@ -9,6 +9,10 @@ from pathlib import Path PROJECT_DIR = Path(__file__).resolve().parents[2] ENV_FILE = PROJECT_DIR / ".env" +DEFAULT_ACORE_STANDARD_REPO = "https://github.com/azerothcore/azerothcore-wotlk.git" +DEFAULT_ACORE_PLAYERBOTS_REPO = "https://github.com/mod-playerbots/azerothcore-wotlk.git" +DEFAULT_ACORE_STANDARD_BRANCH = "master" +DEFAULT_ACORE_PLAYERBOTS_BRANCH = "Playerbot" def load_env(): env = {} @@ -150,6 +154,195 @@ def volume_info(name, fallback=None): pass return {"name": name, "exists": False, "mountpoint": "-"} +def detect_source_variant(env): + variant = read_env(env, "STACK_SOURCE_VARIANT", "").strip().lower() + if variant in ("playerbots", "playerbot"): + return "playerbots" + if variant == "core": + return "core" + if read_env(env, "STACK_IMAGE_MODE", "").strip().lower() == "playerbots": + return "playerbots" + if read_env(env, "MODULE_PLAYERBOTS", "0") == "1" or read_env(env, "PLAYERBOT_ENABLED", "0") == "1": + return "playerbots" + return "core" + +def repo_config_for_variant(env, variant): + if variant == "playerbots": + repo = read_env(env, "ACORE_REPO_PLAYERBOTS", DEFAULT_ACORE_PLAYERBOTS_REPO) + branch = read_env(env, "ACORE_BRANCH_PLAYERBOTS", DEFAULT_ACORE_PLAYERBOTS_BRANCH) + else: + repo = read_env(env, "ACORE_REPO_STANDARD", DEFAULT_ACORE_STANDARD_REPO) + branch = read_env(env, "ACORE_BRANCH_STANDARD", DEFAULT_ACORE_STANDARD_BRANCH) + return repo, branch + +def image_labels(image): + try: + result = subprocess.run( + ["docker", "image", "inspect", "--format", "{{json .Config.Labels}}", image], + capture_output=True, + text=True, + check=True, + timeout=3, + ) + labels = json.loads(result.stdout or "{}") + if isinstance(labels, dict): + return {k: (v or "").strip() for k, v in labels.items()} + except Exception: + pass + return {} + +def first_label(labels, keys): + for key in keys: + value = labels.get(key, "") + if value: + return value + return "" + +def short_commit(commit): + commit = commit.strip() + if re.fullmatch(r"[0-9a-fA-F]{12,}", commit): + return commit[:12] + return commit + +def git_info_from_path(path): + repo_path = Path(path) + if not (repo_path / ".git").exists(): + return None + + def run_git(args): + try: + result = subprocess.run( + ["git"] + args, + cwd=repo_path, + capture_output=True, + text=True, + check=True, + ) + return result.stdout.strip() + except Exception: + return "" + + commit = run_git(["rev-parse", "HEAD"]) + if not commit: + return None + + return { + "commit": commit, + "commit_short": run_git(["rev-parse", "--short", "HEAD"]) or short_commit(commit), + "date": run_git(["log", "-1", "--format=%cd", "--date=iso-strict"]), + "repo": run_git(["remote", "get-url", "origin"]), + "branch": run_git(["rev-parse", "--abbrev-ref", "HEAD"]), + "path": str(repo_path), + } + +def candidate_source_paths(env, variant): + paths = [] + for key in ("MODULES_REBUILD_SOURCE_PATH", "SOURCE_DIR"): + value = read_env(env, key, "") + if value: + paths.append(value) + + local_root = read_env(env, "STORAGE_PATH_LOCAL", "./local-storage") + primary_dir = "azerothcore-playerbots" if variant == "playerbots" else "azerothcore" + fallback_dir = "azerothcore" if variant == "playerbots" else "azerothcore-playerbots" + paths.append(os.path.join(local_root, "source", primary_dir)) + paths.append(os.path.join(local_root, "source", fallback_dir)) + + normalized = [] + for p in paths: + expanded = expand_path(p, env) + try: + normalized.append(str(Path(expanded).expanduser().resolve())) + except Exception: + normalized.append(str(Path(expanded).expanduser())) + # Deduplicate while preserving order + seen = set() + unique_paths = [] + for p in normalized: + if p not in seen: + seen.add(p) + unique_paths.append(p) + return unique_paths + +def build_info(service_data, env): + variant = detect_source_variant(env) + repo, branch = repo_config_for_variant(env, variant) + info = { + "variant": variant, + "repo": repo, + "branch": branch, + "image": "", + "commit": "", + "commit_date": "", + "commit_source": "", + "source_path": "", + } + + image_candidates = [] + for svc in service_data: + if svc.get("name") in ("ac-worldserver", "ac-authserver", "ac-db-import"): + image = svc.get("image") or "" + if image: + image_candidates.append(image) + + for env_key in ( + "AC_WORLDSERVER_IMAGE_PLAYERBOTS", + "AC_WORLDSERVER_IMAGE_MODULES", + "AC_WORLDSERVER_IMAGE", + "AC_AUTHSERVER_IMAGE_PLAYERBOTS", + "AC_AUTHSERVER_IMAGE_MODULES", + "AC_AUTHSERVER_IMAGE", + ): + value = read_env(env, env_key, "") + if value: + image_candidates.append(value) + + seen = set() + deduped_images = [] + for img in image_candidates: + if img not in seen: + seen.add(img) + deduped_images.append(img) + + commit_label_keys = [ + "build.source_commit", + "org.opencontainers.image.revision", + "org.opencontainers.image.version", + ] + date_label_keys = [ + "build.source_date", + "org.opencontainers.image.created", + "build.timestamp", + ] + + for image in deduped_images: + labels = image_labels(image) + if not info["image"]: + info["image"] = image + if not labels: + continue + commit = short_commit(first_label(labels, commit_label_keys)) + date = first_label(labels, date_label_keys) + if commit or date: + info["commit"] = commit + info["commit_date"] = date + info["commit_source"] = "image-label" + info["image"] = image + return info + + for path in candidate_source_paths(env, variant): + git_meta = git_info_from_path(path) + if git_meta: + info["commit"] = git_meta.get("commit_short") or short_commit(git_meta.get("commit", "")) + info["commit_date"] = git_meta.get("date", "") + info["commit_source"] = "source-tree" + info["source_path"] = git_meta.get("path", "") + info["repo"] = git_meta.get("repo") or info["repo"] + info["branch"] = git_meta.get("branch") or info["branch"] + return info + + return info + def expand_path(value, env): storage = read_env(env, "STORAGE_PATH", "./storage") local_storage = read_env(env, "STORAGE_PATH_LOCAL", "./local-storage") @@ -274,6 +467,8 @@ def main(): "mysql_data": volume_info(f"{project}_mysql-data", "mysql-data"), } + build = build_info(service_data, env) + data = { "timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), "project": project, @@ -285,6 +480,7 @@ def main(): "volumes": volumes, "users": user_stats(env), "stats": docker_stats(), + "build": build, } print(json.dumps(data)) diff --git a/scripts/go/statusdash.go b/scripts/go/statusdash.go index db713b8..acf8a98 100644 --- a/scripts/go/statusdash.go +++ b/scripts/go/statusdash.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "log" + "net" + "os" "os/exec" "strings" "time" @@ -61,6 +63,17 @@ type Module struct { Type string `json:"type"` } +type BuildInfo struct { + Variant string `json:"variant"` + Repo string `json:"repo"` + Branch string `json:"branch"` + Image string `json:"image"` + Commit string `json:"commit"` + CommitDate string `json:"commit_date"` + CommitSource string `json:"commit_source"` + SourcePath string `json:"source_path"` +} + type Snapshot struct { Timestamp string `json:"timestamp"` Project string `json:"project"` @@ -72,6 +85,7 @@ type Snapshot struct { Volumes map[string]VolumeInfo `json:"volumes"` Users UserStats `json:"users"` Stats map[string]ContainerStats `json:"stats"` + Build BuildInfo `json:"build"` } var persistentServiceOrder = []string{ @@ -84,6 +98,81 @@ var persistentServiceOrder = []string{ "ac-backup", } +func humanDuration(d time.Duration) string { + if d < time.Minute { + return "<1m" + } + days := d / (24 * time.Hour) + d -= days * 24 * time.Hour + hours := d / time.Hour + d -= hours * time.Hour + mins := d / time.Minute + + switch { + case days > 0: + return fmt.Sprintf("%dd %dh", days, hours) + case hours > 0: + return fmt.Sprintf("%dh %dm", hours, mins) + default: + return fmt.Sprintf("%dm", mins) + } +} + +func formatUptime(startedAt string) string { + if startedAt == "" { + return "-" + } + parsed, err := time.Parse(time.RFC3339Nano, startedAt) + if err != nil { + parsed, err = time.Parse(time.RFC3339, startedAt) + if err != nil { + return "-" + } + } + if parsed.IsZero() { + return "-" + } + uptime := time.Since(parsed) + if uptime < 0 { + uptime = 0 + } + return humanDuration(uptime) +} + +func primaryIPv4() string { + ifaces, err := net.Interfaces() + if err != nil { + return "" + } + for _, iface := range ifaces { + if iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 { + continue + } + addrs, err := iface.Addrs() + if err != nil { + continue + } + for _, addr := range addrs { + var ip net.IP + switch v := addr.(type) { + case *net.IPNet: + ip = v.IP + case *net.IPAddr: + ip = v.IP + } + if ip == nil || ip.IsLoopback() { + continue + } + ip = ip.To4() + if ip == nil { + continue + } + return ip.String() + } + } + return "" +} + func runSnapshot() (*Snapshot, error) { cmd := exec.Command("./scripts/bash/statusjson.sh") output, err := cmd.Output() @@ -126,8 +215,8 @@ func buildServicesTable(s *Snapshot) *TableNoCol { runningServices, setupServices := partitionServices(s.Services) table := NewTableNoCol() - rows := [][]string{{"Group", "Service", "Status", "Health", "CPU%", "Memory"}} - appendRows := func(groupLabel string, services []Service) { + rows := [][]string{{"Service", "Status", "Health", "Uptime", "CPU%", "Memory"}} + appendRows := func(services []Service) { for _, svc := range services { cpu := "-" mem := "-" @@ -139,12 +228,12 @@ func buildServicesTable(s *Snapshot) *TableNoCol { if svc.Status != "running" && svc.ExitCode != "0" && svc.ExitCode != "" { health = fmt.Sprintf("%s (%s)", svc.Health, svc.ExitCode) } - rows = append(rows, []string{groupLabel, svc.Label, svc.Status, health, cpu, mem}) + rows = append(rows, []string{svc.Label, svc.Status, health, formatUptime(svc.StartedAt), cpu, mem}) } } - appendRows("Persistent", runningServices) - appendRows("Setup", setupServices) + appendRows(runningServices) + appendRows(setupServices) table.Rows = rows table.RowSeparator = false @@ -223,9 +312,11 @@ func buildStorageParagraph(s *Snapshot) *widgets.Paragraph { } par := widgets.NewParagraph() par.Title = "Storage" - par.Text = b.String() + par.Text = strings.TrimRight(b.String(), "\n") par.Border = true par.BorderStyle = ui.NewStyle(ui.ColorYellow) + par.PaddingLeft = 0 + par.PaddingRight = 0 return par } @@ -247,13 +338,75 @@ func buildVolumesParagraph(s *Snapshot) *widgets.Paragraph { } par := widgets.NewParagraph() par.Title = "Volumes" - par.Text = b.String() + par.Text = strings.TrimRight(b.String(), "\n") + par.Border = true + par.BorderStyle = ui.NewStyle(ui.ColorYellow) + par.PaddingLeft = 0 + par.PaddingRight = 0 + return par +} + +func simplifyRepo(repo string) string { + repo = strings.TrimSpace(repo) + repo = strings.TrimSuffix(repo, ".git") + repo = strings.TrimPrefix(repo, "https://") + repo = strings.TrimPrefix(repo, "http://") + repo = strings.TrimPrefix(repo, "git@") + repo = strings.TrimPrefix(repo, "github.com:") + repo = strings.TrimPrefix(repo, "gitlab.com:") + repo = strings.TrimPrefix(repo, "github.com/") + repo = strings.TrimPrefix(repo, "gitlab.com/") + return repo +} + +func buildInfoParagraph(s *Snapshot) *widgets.Paragraph { + build := s.Build + var lines []string + + if build.Branch != "" { + lines = append(lines, fmt.Sprintf("Branch: %s", build.Branch)) + } + + if repo := simplifyRepo(build.Repo); repo != "" { + lines = append(lines, fmt.Sprintf("Repo: %s", repo)) + } + + commitLine := "Git: unknown" + if build.Commit != "" { + commitLine = fmt.Sprintf("Git: %s", build.Commit) + switch build.CommitSource { + case "image-label": + commitLine += " [image]" + case "source-tree": + commitLine += " [source]" + } + } + lines = append(lines, commitLine) + + if build.Image != "" { + // Skip image line to keep header compact + } + + lines = append(lines, fmt.Sprintf("Updated: %s", s.Timestamp)) + + par := widgets.NewParagraph() + par.Title = "Build" + par.Text = strings.Join(lines, "\n") par.Border = true par.BorderStyle = ui.NewStyle(ui.ColorYellow) return par } func renderSnapshot(s *Snapshot, selectedModule int) (*widgets.List, *ui.Grid) { + hostname, err := os.Hostname() + if err != nil || hostname == "" { + hostname = "unknown" + } + ip := primaryIPv4() + if ip == "" { + ip = "unknown" + } + servicesTable := buildServicesTable(s) portsTable := buildPortsTable(s) for i := 1; i < len(portsTable.Rows); i++ { @@ -287,43 +440,43 @@ func renderSnapshot(s *Snapshot, selectedModule int) (*widgets.List, *ui.Grid) { 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.Text = fmt.Sprintf("Host: %s\nIP: %s\nProject: %s\nNetwork: %s", hostname, ip, s.Project, s.Network) header.Border = true + buildPar := buildInfoParagraph(s) + 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.Title = "Users" + usersPar.Text = fmt.Sprintf(" 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.15, - ui.NewCol(0.6, header), - ui.NewCol(0.4, usersPar), + ui.NewRow(0.18, + ui.NewCol(0.34, header), + ui.NewCol(0.33, buildPar), + ui.NewCol(0.33, usersPar), ), - ui.NewRow(0.46, + ui.NewRow(0.43, ui.NewCol(0.6, servicesTable), ui.NewCol(0.4, portsTable), ), ui.NewRow(0.39, ui.NewCol(0.25, modulesList), ui.NewCol(0.15, - ui.NewRow(0.30, helpPar), - ui.NewRow(0.70, moduleInfoPar), + ui.NewRow(0.32, helpPar), + ui.NewRow(0.68, moduleInfoPar), ), ui.NewCol(0.6, - ui.NewRow(0.55, + ui.NewRow(0.513, ui.NewCol(1.0, storagePar), ), - ui.NewRow(0.45, + ui.NewRow(0.487, ui.NewCol(1.0, volumesPar), ), ),