┌─ GO ───────────────────────────────────────────────────────────────────────┐
│ package cmd │
│ │
│ import ( │
│ "fmt" │
│ "ookstats/internal/blizzard" │
│ "ookstats/internal/database" │
│ "ookstats/internal/pipeline" │
│ │
│ "github.com/spf13/cobra" │
│ ) │
│ │
│ var processCmd = &cobra.Command{ │
│ Use: "process", │
│ Short: "Process and compute data", │
│ Long: `Process raw data and compute player rankings, aggregations, etc.`, │
│ } │
│ │
│ var processAllCmd = &cobra.Command{ │
│ Use: "all", │
│ Short: "Process all data (players + rankings)", │
│ Long: `Process all data: player aggregations, player rankings, and run rankings. │
│ `, │
│ RunE: func(cmd *cobra.Command, args []string) error { │
│ fmt.Println("=== Complete Data Processing ===") │
│ │
│ // step 1: process players (aggregations + rankings) │
│ fmt.Println("\n=== Step 1: Processing Players ===") │
│ if err := processPlayersCmd.RunE(cmd, args); err != nil { │
│ return fmt.Errorf("player processing failed: %w", err) │
│ } │
│ │
│ // step 2: process run rankings │
│ fmt.Println("\n=== Step 2: Processing Run Rankings ===") │
│ if err := processRankingsCmd.RunE(cmd, args); err != nil { │
│ return fmt.Errorf("run ranking processing failed: %w", err) │
│ } │
│ │
│ fmt.Printf("\n[OK] Complete data processing finished!\n") │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats generate api --out web/public' to update JSON fil │
│ es\n") │
│ fmt.Printf(" 2. Build and deploy the website\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processRankingsCmd = &cobra.Command{ │
│ Use: "rankings", │
│ Short: "Compute run rankings", │
│ Long: `Compute global, regional, and realm rankings for all challenge mode runs. │
│ `, │
│ RunE: func(cmd *cobra.Command, args []string) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to database: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", database.DBFilePath()) │
│ │
│ verbose, _ := cmd.InheritedFlags().GetBool("verbose") │
│ opts := pipeline.ProcessRunRankingsOptions{ │
│ Verbose: verbose, │
│ } │
│ │
│ if err := pipeline.ProcessRunRankings(db, opts); err != nil { │
│ return err │
│ } │
│ │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats generate api --out web/public' to update JSON fil │
│ es\n") │
│ fmt.Printf(" 2. Build and deploy the website\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processPlayersCmd = &cobra.Command{ │
│ Use: "players", │
│ Short: "Aggregate player statistics", │
│ Long: `Aggregate player data including best runs, combined times, and rankings.` │
│ , │
│ RunE: func(cmd *cobra.Command, args []string) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to database: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", database.DBFilePath()) │
│ │
│ verbose, _ := cmd.InheritedFlags().GetBool("verbose") │
│ opts := pipeline.ProcessPlayersOptions{ │
│ Verbose: verbose, │
│ } │
│ │
│ profilesCreated, qualifiedPlayers, err := pipeline.ProcessPlayers(db, opts) │
│ if err != nil { │
│ return err │
│ } │
│ │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats process profiles' to fetch player details\n") │
│ fmt.Printf(" 2. Test your Astro DB integration\n") │
│ │
│ // Suppress unused variable warnings │
│ _ = profilesCreated │
│ _ = qualifiedPlayers │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processProfilesCmd = &cobra.Command{ │
│ Use: "profiles", │
│ Short: "Fetch player profiles from Blizzard API", │
│ Long: `Fetch detailed player information, equipment, and avatars for players wit │
│ h complete dungeon coverage.`, │
│ RunE: func(cmd *cobra.Command, args []string) error { │
│ fmt.Println("=== Player Profile Fetcher ===") │
│ │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to database: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", database.DBFilePath()) │
│ │
│ // check if we have eligible players │
│ var eligibleCount int │
│ err = db.QueryRow("SELECT COUNT(*) FROM player_profiles WHERE has_complete_cover │
│ age = 1").Scan(&eligibleCount) │
│ if err != nil { │
│ return fmt.Errorf("failed to count eligible players: %w", err) │
│ } │
│ │
│ fmt.Printf("Found %d eligible players with complete coverage\n", eligibleCount) │
│ │
│ if eligibleCount == 0 { │
│ return fmt.Errorf("no eligible players found - run 'process players' first") │
│ } │
│ │
│ fmt.Printf("Player profile fetching not yet implemented\n") │
│ fmt.Printf("This command will fetch player details from Blizzard API for %d play │
│ ers\n", eligibleCount) │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Implement Blizzard API client integration\n") │
│ fmt.Printf(" 2. Add concurrent fetching with rate limiting\n") │
│ fmt.Printf(" 3. Store player_details and player_equipment data\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processAccountsCmd = &cobra.Command{ │
│ Use: "accounts", │
│ Short: "Group characters into accounts via account-wide achievement tuples", │
│ Long: `Backfills account fingerprints for any character that has a per-character │
│ fingerprint but no account fingerprint yet (one Blizzard API call per │
│ candidate), then runs the in-memory connected-components grouping over every │
│ stored fingerprint and rebuilds the accounts table + players.account_id.`, │
│ RunE: func(cmd *cobra.Command, args []string) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("connect db: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ client, err := blizzard.NewClient() │
│ if err != nil { │
│ return fmt.Errorf("blizzard client: %w", err) │
│ } │
│ │
│ maxShare, _ := cmd.Flags().GetFloat64("max-tuple-share") │
│ minMatches, _ := cmd.Flags().GetInt("min-edge-matches") │
│ workers, _ := cmd.Flags().GetInt("backfill-workers") │
│ maxBackfill, _ := cmd.Flags().GetInt("max-backfill") │
│ skipBackfill, _ := cmd.Flags().GetBool("skip-backfill") │
│ manualLinks, _ := cmd.Flags().GetString("manual-links") │
│ │
│ opts := pipeline.AccountGroupingOptions{ │
│ MaxTupleShare: maxShare, │
│ MinEdgeMatches: minMatches, │
│ BackfillWorkers: workers, │
│ MaxBackfill: maxBackfill, │
│ ManualLinksPath: manualLinks, │
│ } │
│ │
│ var c *blizzard.Client │
│ if !skipBackfill { │
│ c = client │
│ } │
│ res, err := pipeline.ProcessAccounts(database.NewDatabaseService(db), c, opts) │
│ if err != nil { │
│ return err │
│ } │
│ fmt.Printf("\n=== Account grouping complete ===\n") │
│ fmt.Printf("Backfilled: %d (errors: %d)\n", res.Backfilled, res.BackfillErrors) │
│ fmt.Printf("Characters: %d\n", res.Characters) │
│ fmt.Printf("Accounts: %d\n", res.Accounts) │
│ fmt.Printf("Multi-character accounts: %d\n", res.MultiCharAccounts) │
│ fmt.Printf("Duration: %s\n", res.Duration.Round(1e9)) │
│ return nil │
│ }, │
│ } │
│ │
│ func init() { │
│ rootCmd.AddCommand(processCmd) │
│ processCmd.AddCommand(processAllCmd) │
│ processCmd.AddCommand(processRankingsCmd) │
│ processCmd.AddCommand(processPlayersCmd) │
│ processCmd.AddCommand(processProfilesCmd) │
│ │
│ processCmd.AddCommand(processAccountsCmd) │
│ processAccountsCmd.Flags().Float64("max-tuple-share", 0.05, "Drop (id, ts) tuples │
│ seen in more than this fraction of population") │
│ processAccountsCmd.Flags().Int("min-edge-matches", 5, "Require N matching tuples │
│ between two characters before unioning them") │
│ processAccountsCmd.Flags().Int("backfill-workers", 20, "Concurrent achievement fe │
│ tches during backfill") │
│ processAccountsCmd.Flags().Int("max-backfill", 0, "Cap backfill fetches per run ( │
│ 0 = unlimited)") │
│ processAccountsCmd.Flags().Bool("skip-backfill", false, "Skip the backfill pass; │
│ only re-run grouping over existing data") │
│ processAccountsCmd.Flags().String("manual-links", "account-manual-links.json", "P │
│ ath to curator-supplied manual account links JSON (missing file is OK)") │
│ } │
└────────────────────────────────────────────────────────────────────────────────────┘
┌─ GO ─────────────────────────────────┐
│ package cmd │
│ │
│ import ( │
│ "fmt" │
│ "ookstats/internal/blizzard" │
│ "ookstats/internal/database" │
│ "ookstats/internal/pipeline" │
│ │
│ "github.com/spf13/cobra" │
│ ) │
│ │
│ var processCmd = &cobra.Command{ │
│ Use: "process", │
│ Short: "Process and compute data", │
│ Long: `Process raw data and compute player │
│ rankings, aggregations, etc.`, │
│ } │
│ │
│ var processAllCmd = &cobra.Command{ │
│ Use: "all", │
│ Short: "Process all data (players + ranking │
│ s)", │
│ Long: `Process all data: player aggregatio │
│ ns, player rankings, and run rankings.`, │
│ RunE: func(cmd *cobra.Command, args []strin │
│ g) error { │
│ fmt.Println("=== Complete Data Processing │
│ ===") │
│ │
│ // step 1: process players (aggregations + │
│ rankings) │
│ fmt.Println("\n=== Step 1: Processing Play │
│ ers ===") │
│ if err := processPlayersCmd.RunE(cmd, args │
│ ); err != nil { │
│ return fmt.Errorf("player processing fail │
│ ed: %w", err) │
│ } │
│ │
│ // step 2: process run rankings │
│ fmt.Println("\n=== Step 2: Processing Run │
│ Rankings ===") │
│ if err := processRankingsCmd.RunE(cmd, arg │
│ s); err != nil { │
│ return fmt.Errorf("run ranking processing │
│ failed: %w", err) │
│ } │
│ │
│ fmt.Printf("\n[OK] Complete data processin │
│ g finished!\n") │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats generate ap │
│ i --out web/public' to update JSON files\n") │
│ fmt.Printf(" 2. Build and deploy the webs │
│ ite\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processRankingsCmd = &cobra.Command{ │
│ Use: "rankings", │
│ Short: "Compute run rankings", │
│ Long: `Compute global, regional, and realm │
│ rankings for all challenge mode runs.`, │
│ RunE: func(cmd *cobra.Command, args []strin │
│ g) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to d │
│ atabase: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", │
│ database.DBFilePath()) │
│ │
│ verbose, _ := cmd.InheritedFlags().GetBool │
│ ("verbose") │
│ opts := pipeline.ProcessRunRankingsOptions │
│ { │
│ Verbose: verbose, │
│ } │
│ │
│ if err := pipeline.ProcessRunRankings(db, │
│ opts); err != nil { │
│ return err │
│ } │
│ │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats generate ap │
│ i --out web/public' to update JSON files\n") │
│ fmt.Printf(" 2. Build and deploy the webs │
│ ite\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processPlayersCmd = &cobra.Command{ │
│ Use: "players", │
│ Short: "Aggregate player statistics", │
│ Long: `Aggregate player data including bes │
│ t runs, combined times, and rankings.`, │
│ RunE: func(cmd *cobra.Command, args []strin │
│ g) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to d │
│ atabase: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", │
│ database.DBFilePath()) │
│ │
│ verbose, _ := cmd.InheritedFlags().GetBool │
│ ("verbose") │
│ opts := pipeline.ProcessPlayersOptions{ │
│ Verbose: verbose, │
│ } │
│ │
│ profilesCreated, qualifiedPlayers, err := │
│ pipeline.ProcessPlayers(db, opts) │
│ if err != nil { │
│ return err │
│ } │
│ │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Run 'ookstats process pro │
│ files' to fetch player details\n") │
│ fmt.Printf(" 2. Test your Astro DB integr │
│ ation\n") │
│ │
│ // Suppress unused variable warnings │
│ _ = profilesCreated │
│ _ = qualifiedPlayers │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processProfilesCmd = &cobra.Command{ │
│ Use: "profiles", │
│ Short: "Fetch player profiles from Blizzard │
│ API", │
│ Long: `Fetch detailed player information, │
│ equipment, and avatars for players with comp │
│ lete dungeon coverage.`, │
│ RunE: func(cmd *cobra.Command, args []strin │
│ g) error { │
│ fmt.Println("=== Player Profile Fetcher == │
│ =") │
│ │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("failed to connect to d │
│ atabase: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ fmt.Printf("Connected to database: %s\n", │
│ database.DBFilePath()) │
│ │
│ // check if we have eligible players │
│ var eligibleCount int │
│ err = db.QueryRow("SELECT COUNT(*) FROM pl │
│ ayer_profiles WHERE has_complete_coverage = │
│ 1").Scan(&eligibleCount) │
│ if err != nil { │
│ return fmt.Errorf("failed to count eligib │
│ le players: %w", err) │
│ } │
│ │
│ fmt.Printf("Found %d eligible players with │
│ complete coverage\n", eligibleCount) │
│ │
│ if eligibleCount == 0 { │
│ return fmt.Errorf("no eligible players fo │
│ und - run 'process players' first") │
│ } │
│ │
│ fmt.Printf("Player profile fetching not ye │
│ t implemented\n") │
│ fmt.Printf("This command will fetch player │
│ details from Blizzard API for %d players\n" │
│ , eligibleCount) │
│ fmt.Printf("\nNext steps:\n") │
│ fmt.Printf(" 1. Implement Blizzard API cl │
│ ient integration\n") │
│ fmt.Printf(" 2. Add concurrent fetching w │
│ ith rate limiting\n") │
│ fmt.Printf(" 3. Store player_details and │
│ player_equipment data\n") │
│ │
│ return nil │
│ }, │
│ } │
│ │
│ var processAccountsCmd = &cobra.Command{ │
│ Use: "accounts", │
│ Short: "Group characters into accounts via │
│ account-wide achievement tuples", │
│ Long: `Backfills account fingerprints for a │
│ ny character that has a per-character │
│ fingerprint but no account fingerprint yet ( │
│ one Blizzard API call per │
│ candidate), then runs the in-memory connecte │
│ d-components grouping over every │
│ stored fingerprint and rebuilds the accounts │
│ table + players.account_id.`, │
│ RunE: func(cmd *cobra.Command, args []strin │
│ g) error { │
│ db, err := database.Connect() │
│ if err != nil { │
│ return fmt.Errorf("connect db: %w", err) │
│ } │
│ defer db.Close() │
│ │
│ client, err := blizzard.NewClient() │
│ if err != nil { │
│ return fmt.Errorf("blizzard client: %w", │
│ err) │
│ } │
│ │
│ maxShare, _ := cmd.Flags().GetFloat64("max │
│ -tuple-share") │
│ minMatches, _ := cmd.Flags().GetInt("min-e │
│ dge-matches") │
│ workers, _ := cmd.Flags().GetInt("backfill │
│ -workers") │
│ maxBackfill, _ := cmd.Flags().GetInt("max- │
│ backfill") │
│ skipBackfill, _ := cmd.Flags().GetBool("sk │
│ ip-backfill") │
│ manualLinks, _ := cmd.Flags().GetString("m │
│ anual-links") │
│ │
│ opts := pipeline.AccountGroupingOptions{ │
│ MaxTupleShare: maxShare, │
│ MinEdgeMatches: minMatches, │
│ BackfillWorkers: workers, │
│ MaxBackfill: maxBackfill, │
│ ManualLinksPath: manualLinks, │
│ } │
│ │
│ var c *blizzard.Client │
│ if !skipBackfill { │
│ c = client │
│ } │
│ res, err := pipeline.ProcessAccounts(datab │
│ ase.NewDatabaseService(db), c, opts) │
│ if err != nil { │
│ return err │
│ } │
│ fmt.Printf("\n=== Account grouping complet │
│ e ===\n") │
│ fmt.Printf("Backfilled: %d (errors: %d)\n" │
│ , res.Backfilled, res.BackfillErrors) │
│ fmt.Printf("Characters: %d\n", res.Charact │
│ ers) │
│ fmt.Printf("Accounts: %d\n", res.Account │
│ s) │
│ fmt.Printf("Multi-character accounts: %d\n │
│ ", res.MultiCharAccounts) │
│ fmt.Printf("Duration: %s\n", res.Duratio │
│ n.Round(1e9)) │
│ return nil │
│ }, │
│ } │
│ │
│ func init() { │
│ rootCmd.AddCommand(processCmd) │
│ processCmd.AddCommand(processAllCmd) │
│ processCmd.AddCommand(processRankingsCmd) │
│ processCmd.AddCommand(processPlayersCmd) │
│ processCmd.AddCommand(processProfilesCmd) │
│ │
│ processCmd.AddCommand(processAccountsCmd) │
│ processAccountsCmd.Flags().Float64("max-tup │
│ le-share", 0.05, "Drop (id, ts) tuples seen │
│ in more than this fraction of population") │
│ processAccountsCmd.Flags().Int("min-edge-ma │
│ tches", 5, "Require N matching tuples betwee │
│ n two characters before unioning them") │
│ processAccountsCmd.Flags().Int("backfill-wo │
│ rkers", 20, "Concurrent achievement fetches │
│ during backfill") │
│ processAccountsCmd.Flags().Int("max-backfil │
│ l", 0, "Cap backfill fetches per run (0 = un │
│ limited)") │
│ processAccountsCmd.Flags().Bool("skip-backf │
│ ill", false, "Skip the backfill pass; only r │
│ e-run grouping over existing data") │
│ processAccountsCmd.Flags().String("manual-l │
│ inks", "account-manual-links.json", "Path to │
│ curator-supplied manual account links JSON │
│ (missing file is OK)") │
│ } │
└──────────────────────────────────────────────┘