OOKNET                             [ /  search the index  ]  
──────────────────────────────────────────────────────────────────────────────────────
══════════════════════════════════════════════════════════════════════════════════════
OOKNET   [ /  search  ]  
────────────────────────────────────────────────
════════════════════════════════════════════════
 
 
master @ 351 LINES
 
[ HISTORY ]  [ UP ]
 

package cmd

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/charmbracelet/log"
	"github.com/spf13/cobra"
	"ookstats/internal/blizzard"
	"ookstats/internal/database"
)

// analyzeCmd summarizes CM fetch coverage to power the status API.
var analyzeCmd = &cobra.Command{
	Use:   "analyze",
	Short: "Summarize CM fetch coverage and write status JSON",
	Long:  `Reads previously recorded fetch results and outputs per-realm/dungeon cov
erage data for the status page.`,
	RunE: func(cmd *cobra.Command, args []string) error {
		outPath, _ := cmd.Flags().GetString("out")
		statusDir, _ := cmd.Flags().GetString("status-dir")
		regionsCSV, _ := cmd.Flags().GetString("regions")
		periodsCSV, _ := cmd.Flags().GetString("periods")
		rng, _ := cmd.Flags().GetString("range")
		concurrency, _ := cmd.Flags().GetInt("concurrency") // retained for compatibilit
y
		_ = concurrency

		db, err := database.Connect()
		if err != nil {
			return fmt.Errorf("db connect: %w", err)
		}
		defer db.Close()

		_, dungeons := blizzard.GetHardcodedPeriodAndDungeons()

		realms := blizzard.GetAllRealms()
		if strings.TrimSpace(regionsCSV) != "" {
			allowed := map[string]bool{}
			for _, r := range strings.Split(regionsCSV, ",") {
				r = strings.ToLower(strings.TrimSpace(r))
				if r != "" {
					allowed[r] = true
				}
			}
			for slug, info := range realms {
				if !allowed[strings.ToLower(info.Region)] {
					delete(realms, slug)
				}
			}
		}

		var periodsSpec string
		if strings.TrimSpace(periodsCSV) != "" {
			periodsSpec = periodsCSV
		} else if strings.TrimSpace(rng) != "" {
			parts := strings.Split(strings.TrimSpace(rng), "-")
			if len(parts) != 2 {
				return errors.New("invalid --range format (expected start-end)")
			}
			a, err1 := strconv.Atoi(strings.TrimSpace(parts[0]))
			b, err2 := strconv.Atoi(strings.TrimSpace(parts[1]))
			if err1 != nil || err2 != nil || a <= 0 || b <= 0 || b < a {
				return errors.New("invalid --range values")
			}
			var list []string
			for i := a; i <= b; i++ {
				list = append(list, fmt.Sprintf("%d", i))
			}
			periodsSpec = strings.Join(list, ",")
		}

		return runAnalyze(db, realms, dungeons, periodsSpec, outPath, statusDir)
	},
}

func init() {
	rootCmd.AddCommand(analyzeCmd)
	analyzeCmd.Flags().String("out", "", "Optional path to write status JSON (default
: {status-dir}/latest-runs.json)")
	analyzeCmd.Flags().String("status-dir", "web/public/api/status", "Base directory 
for status JSON files")
	analyzeCmd.Flags().String("regions", "", "Comma-separated regions to include (us,
eu,kr,tw)")
	analyzeCmd.Flags().String("periods", "", "Comma-separated period IDs to include")
	analyzeCmd.Flags().String("range", "", "Period range to include (e.g., 1026-1030)
")
	analyzeCmd.Flags().Int("concurrency", 0, "Deprecated (no longer used)")
}

type statusDungeonEntry struct {
	DungeonID    int    `json:"dungeon_id"`
	DungeonSlug  string `json:"dungeon_slug"`
	DungeonName  string `json:"dungeon_name"`
	Status       string `json:"status"`
	Periods      []int  `json:"periods"`
	Missing      []int  `json:"missing_periods"`
	ErrorPeriods []int  `json:"error_periods"`
}

type statusRealmEntry struct {
	Region         string               `json:"region"`
	RealmSlug      string               `json:"realm_slug"`
	RealmName      string               `json:"realm_name"`
	Health         string               `json:"health"`
	TotalPeriods   int                  `json:"total_periods"`
	MissingPeriods int                  `json:"missing_periods"`
	ErrorPeriods   int                  `json:"error_periods"`
	Dungeons       []statusDungeonEntry `json:"dungeons"`
}

type statusPayload struct {
	GeneratedAt string             `json:"generated_at"`
	Realms      []statusRealmEntry `json:"realms"`
}

func runAnalyze(db *sql.DB, realms map[string]blizzard.RealmInfo, dungeons []blizz
ard.DungeonInfo, periodsSpec string, outPath, statusDir string) error {
	log.Info("building status coverage", "realms", len(realms), "dungeons", len(dunge
ons))

	allowedPeriods := make(map[int]bool)
	if strings.TrimSpace(periodsSpec) != "" {
		parsed, err := blizzard.ParsePeriods(periodsSpec)
		if err != nil {
			return fmt.Errorf("parse periods: %w", err)
		}
		for _, p := range parsed {
			if pid, err := strconv.Atoi(p); err == nil {
				allowedPeriods[pid] = true
			}
		}
	}
	filterPeriods := len(allowedPeriods) > 0

	realmFilter := make(map[string]blizzard.RealmInfo)
	for _, info := range realms {
		realmFilter[realmMapKey(info.Region, info.Slug)] = info
	}

	dungeonLookup := make(map[int]blizzard.DungeonInfo)
	for _, d := range dungeons {
		dungeonLookup[d.ID] = d
	}

	type dungeonAgg struct {
		info    blizzard.DungeonInfo
		periods []int
		missing []int
		errors  []int
	}
	type realmAgg struct {
		info     blizzard.RealmInfo
		dungeons map[int]*dungeonAgg
	}

	agg := make(map[string]*realmAgg)

	rows, err := db.Query(`SELECT region, realm_slug, dungeon_id, period_id, status F
ROM fetch_status`)
	if err != nil {
		return fmt.Errorf("query fetch_status: %w", err)
	}
	defer rows.Close()

	for rows.Next() {
		var region, realmSlug string
		var dungeonID, periodID int
		var status string
		if err := rows.Scan(&region, &realmSlug, &dungeonID, &periodID, &status); err !=
 nil {
			return fmt.Errorf("scan fetch_status: %w", err)
		}
		if filterPeriods && !allowedPeriods[periodID] {
			continue
		}
		key := realmMapKey(region, realmSlug)
		info, ok := realmFilter[key]
		if !ok {
			continue // skip realms outside requested filters
		}
		ra := agg[key]
		if ra == nil {
			ra = &realmAgg{info: info, dungeons: make(map[int]*dungeonAgg)}
			agg[key] = ra
		}
		dagg := ra.dungeons[dungeonID]
		if dagg == nil {
			dInfo, ok := dungeonLookup[dungeonID]
			if !ok {
				dInfo = blizzard.DungeonInfo{
					ID:   dungeonID,
					Slug: fmt.Sprintf("dungeon-%d", dungeonID),
					Name: fmt.Sprintf("Dungeon %d", dungeonID),
				}
			}
			dagg = &dungeonAgg{info: dInfo}
			ra.dungeons[dungeonID] = dagg
		}
		switch strings.ToLower(status) {
		case "ok":
			dagg.periods = append(dagg.periods, periodID)
		case "missing":
			dagg.missing = append(dagg.missing, periodID)
		case "error":
			dagg.errors = append(dagg.errors, periodID)
		default:
			// treat unknown statuses conservatively as errors
			dagg.errors = append(dagg.errors, periodID)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate fetch_status: %w", err)
	}

	realmKeys := make([]string, 0, len(agg))
	for key := range agg {
		realmKeys = append(realmKeys, key)
	}
	sort.Slice(realmKeys, func(i, j int) bool {
		ri := agg[realmKeys[i]].info
		rj := agg[realmKeys[j]].info
		if ri.Region == rj.Region {
			return ri.Slug < rj.Slug
		}
		return ri.Region < rj.Region
	})

	realmsOut := make([]statusRealmEntry, 0, len(realmKeys))
	for _, key := range realmKeys {
		ra := agg[key]
		realmEntry := statusRealmEntry{
			Region:    ra.info.Region,
			RealmSlug: ra.info.Slug,
			RealmName: ra.info.Name,
		}
		var realmMissing, realmErrors int

		dungeonIDs := make([]int, 0, len(ra.dungeons))
		for id := range ra.dungeons {
			dungeonIDs = append(dungeonIDs, id)
		}
		sort.Ints(dungeonIDs)

		for _, id := range dungeonIDs {
			dagg := ra.dungeons[id]
			sort.Ints(dagg.periods)
			sort.Ints(dagg.missing)
			sort.Ints(dagg.errors)

			status := coverageStatus(len(dagg.periods), len(dagg.missing), len(dagg.errors)
)
			entry := statusDungeonEntry{
				DungeonID:    dagg.info.ID,
				DungeonSlug:  dagg.info.Slug,
				DungeonName:  dagg.info.Name,
				Status:       status,
				Periods:      append([]int(nil), dagg.periods...),
				Missing:      append([]int(nil), dagg.missing...),
				ErrorPeriods: append([]int(nil), dagg.errors...),
			}
			realmEntry.Dungeons = append(realmEntry.Dungeons, entry)
			realmEntry.TotalPeriods += len(dagg.periods)
			realmMissing += len(dagg.missing)
			realmErrors += len(dagg.errors)
		}

		realmEntry.MissingPeriods = realmMissing
		realmEntry.ErrorPeriods = realmErrors
		realmEntry.Health = coverageStatus(realmEntry.TotalPeriods, realmMissing, realmE
rrors)
		realmsOut = append(realmsOut, realmEntry)
	}

	generatedAt := time.Now().UTC().Format(time.RFC3339)
	payload := statusPayload{
		GeneratedAt: generatedAt,
		Realms:      realmsOut,
	}

	if strings.TrimSpace(statusDir) == "" && strings.TrimSpace(outPath) == "" {
		return errors.New("status output path not specified")
	}

	if strings.TrimSpace(outPath) == "" {
		if err := os.MkdirAll(statusDir, 0o755); err != nil {
			return fmt.Errorf("mkdir status dir: %w", err)
		}
		outPath = filepath.Join(statusDir, "latest-runs.json")
	} else if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
		return fmt.Errorf("mkdir output dir: %w", err)
	}

	if err := writeStatusJSON(outPath, payload); err != nil {
		return err
	}

	if strings.TrimSpace(statusDir) != "" {
		for _, realm := range realmsOut {
			dir := filepath.Join(statusDir, realm.Region)
			if err := os.MkdirAll(dir, 0o755); err != nil {
				return fmt.Errorf("mkdir realm dir: %w", err)
			}
			realmPath := filepath.Join(dir, fmt.Sprintf("%s.json", realm.RealmSlug))
			realmPayload := struct {
				GeneratedAt string           `json:"generated_at"`
				Realm       statusRealmEntry `json:"realm"`
			}{
				GeneratedAt: generatedAt,
				Realm:       realm,
			}
			if err := writeStatusJSON(realmPath, realmPayload); err != nil {
				return err
			}
		}
		log.Info("wrote per-realm status files", "dir", statusDir, "count", len(realmsOu
t))
	}

	log.Info("status coverage generated", "realms", len(realmsOut))
	return nil
}

func writeStatusJSON(path string, payload any) error {
	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create %s: %w", path, err)
	}
	defer f.Close()

	enc := json.NewEncoder(f)
	enc.SetIndent("", "  ")
	if err := enc.Encode(payload); err != nil {
		return fmt.Errorf("encode %s: %w", path, err)
	}
	return nil
}

func realmMapKey(region, slug string) string {
	return fmt.Sprintf("%s|%s", strings.ToLower(region), strings.ToLower(slug))
}

func coverageStatus(total, missing, errors int) string {
	if total == 0 {
		return "no_data"
	}
	deficit := missing + errors
	if deficit == 0 {
		return "ok"
	}
	if deficit >= total {
		return "no_data"
	}
	return "some_missing"
}

package cmd

import (
	"database/sql"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/charmbracelet/log"
	"github.com/spf13/cobra"
	"ookstats/internal/blizzard"
	"ookstats/internal/database"
)

// analyzeCmd summarizes CM fetch coverage t
o power the status API.
var analyzeCmd = &cobra.Command{
	Use:   "analyze",
	Short: "Summarize CM fetch coverage and wri
te status JSON",
	Long:  `Reads previously recorded fetch res
ults and outputs per-realm/dungeon coverage 
data for the status page.`,
	RunE: func(cmd *cobra.Command, args []strin
g) error {
		outPath, _ := cmd.Flags().GetString("out")
		statusDir, _ := cmd.Flags().GetString("sta
tus-dir")
		regionsCSV, _ := cmd.Flags().GetString("re
gions")
		periodsCSV, _ := cmd.Flags().GetString("pe
riods")
		rng, _ := cmd.Flags().GetString("range")
		concurrency, _ := cmd.Flags().GetInt("conc
urrency") // retained for compatibility
		_ = concurrency

		db, err := database.Connect()
		if err != nil {
			return fmt.Errorf("db connect: %w", err)
		}
		defer db.Close()

		_, dungeons := blizzard.GetHardcodedPeriod
AndDungeons()

		realms := blizzard.GetAllRealms()
		if strings.TrimSpace(regionsCSV) != "" {
			allowed := map[string]bool{}
			for _, r := range strings.Split(regionsCS
V, ",") {
				r = strings.ToLower(strings.TrimSpace(r)
)
				if r != "" {
					allowed[r] = true
				}
			}
			for slug, info := range realms {
				if !allowed[strings.ToLower(info.Region)
] {
					delete(realms, slug)
				}
			}
		}

		var periodsSpec string
		if strings.TrimSpace(periodsCSV) != "" {
			periodsSpec = periodsCSV
		} else if strings.TrimSpace(rng) != "" {
			parts := strings.Split(strings.TrimSpace(
rng), "-")
			if len(parts) != 2 {
				return errors.New("invalid --range forma
t (expected start-end)")
			}
			a, err1 := strconv.Atoi(strings.TrimSpace
(parts[0]))
			b, err2 := strconv.Atoi(strings.TrimSpace
(parts[1]))
			if err1 != nil || err2 != nil || a <= 0 |
| b <= 0 || b < a {
				return errors.New("invalid --range value
s")
			}
			var list []string
			for i := a; i <= b; i++ {
				list = append(list, fmt.Sprintf("%d", i)
)
			}
			periodsSpec = strings.Join(list, ",")
		}

		return runAnalyze(db, realms, dungeons, pe
riodsSpec, outPath, statusDir)
	},
}

func init() {
	rootCmd.AddCommand(analyzeCmd)
	analyzeCmd.Flags().String("out", "", "Optio
nal path to write status JSON (default: {sta
tus-dir}/latest-runs.json)")
	analyzeCmd.Flags().String("status-dir", "we
b/public/api/status", "Base directory for st
atus JSON files")
	analyzeCmd.Flags().String("regions", "", "C
omma-separated regions to include (us,eu,kr,
tw)")
	analyzeCmd.Flags().String("periods", "", "C
omma-separated period IDs to include")
	analyzeCmd.Flags().String("range", "", "Per
iod range to include (e.g., 1026-1030)")
	analyzeCmd.Flags().Int("concurrency", 0, "D
eprecated (no longer used)")
}

type statusDungeonEntry struct {
	DungeonID    int    `json:"dungeon_id"`
	DungeonSlug  string `json:"dungeon_slug"`
	DungeonName  string `json:"dungeon_name"`
	Status       string `json:"status"`
	Periods      []int  `json:"periods"`
	Missing      []int  `json:"missing_periods"
`
	ErrorPeriods []int  `json:"error_periods"`
}

type statusRealmEntry struct {
	Region         string               `json:"
region"`
	RealmSlug      string               `json:"
realm_slug"`
	RealmName      string               `json:"
realm_name"`
	Health         string               `json:"
health"`
	TotalPeriods   int                  `json:"
total_periods"`
	MissingPeriods int                  `json:"
missing_periods"`
	ErrorPeriods   int                  `json:"
error_periods"`
	Dungeons       []statusDungeonEntry `json:"
dungeons"`
}

type statusPayload struct {
	GeneratedAt string             `json:"gener
ated_at"`
	Realms      []statusRealmEntry `json:"realm
s"`
}

func runAnalyze(db *sql.DB, realms map[strin
g]blizzard.RealmInfo, dungeons []blizzard.Du
ngeonInfo, periodsSpec string, outPath, stat
usDir string) error {
	log.Info("building status coverage", "realm
s", len(realms), "dungeons", len(dungeons))

	allowedPeriods := make(map[int]bool)
	if strings.TrimSpace(periodsSpec) != "" {
		parsed, err := blizzard.ParsePeriods(perio
dsSpec)
		if err != nil {
			return fmt.Errorf("parse periods: %w", er
r)
		}
		for _, p := range parsed {
			if pid, err := strconv.Atoi(p); err == ni
l {
				allowedPeriods[pid] = true
			}
		}
	}
	filterPeriods := len(allowedPeriods) > 0

	realmFilter := make(map[string]blizzard.Rea
lmInfo)
	for _, info := range realms {
		realmFilter[realmMapKey(info.Region, info.
Slug)] = info
	}

	dungeonLookup := make(map[int]blizzard.Dung
eonInfo)
	for _, d := range dungeons {
		dungeonLookup[d.ID] = d
	}

	type dungeonAgg struct {
		info    blizzard.DungeonInfo
		periods []int
		missing []int
		errors  []int
	}
	type realmAgg struct {
		info     blizzard.RealmInfo
		dungeons map[int]*dungeonAgg
	}

	agg := make(map[string]*realmAgg)

	rows, err := db.Query(`SELECT region, realm
_slug, dungeon_id, period_id, status FROM fe
tch_status`)
	if err != nil {
		return fmt.Errorf("query fetch_status: %w"
, err)
	}
	defer rows.Close()

	for rows.Next() {
		var region, realmSlug string
		var dungeonID, periodID int
		var status string
		if err := rows.Scan(&region, &realmSlug, &
dungeonID, &periodID, &status); err != nil {
			return fmt.Errorf("scan fetch_status: %w"
, err)
		}
		if filterPeriods && !allowedPeriods[period
ID] {
			continue
		}
		key := realmMapKey(region, realmSlug)
		info, ok := realmFilter[key]
		if !ok {
			continue // skip realms outside requested
 filters
		}
		ra := agg[key]
		if ra == nil {
			ra = &realmAgg{info: info, dungeons: make
(map[int]*dungeonAgg)}
			agg[key] = ra
		}
		dagg := ra.dungeons[dungeonID]
		if dagg == nil {
			dInfo, ok := dungeonLookup[dungeonID]
			if !ok {
				dInfo = blizzard.DungeonInfo{
					ID:   dungeonID,
					Slug: fmt.Sprintf("dungeon-%d", dungeon
ID),
					Name: fmt.Sprintf("Dungeon %d", dungeon
ID),
				}
			}
			dagg = &dungeonAgg{info: dInfo}
			ra.dungeons[dungeonID] = dagg
		}
		switch strings.ToLower(status) {
		case "ok":
			dagg.periods = append(dagg.periods, perio
dID)
		case "missing":
			dagg.missing = append(dagg.missing, perio
dID)
		case "error":
			dagg.errors = append(dagg.errors, periodI
D)
		default:
			// treat unknown statuses conservatively 
as errors
			dagg.errors = append(dagg.errors, periodI
D)
		}
	}
	if err := rows.Err(); err != nil {
		return fmt.Errorf("iterate fetch_status: %
w", err)
	}

	realmKeys := make([]string, 0, len(agg))
	for key := range agg {
		realmKeys = append(realmKeys, key)
	}
	sort.Slice(realmKeys, func(i, j int) bool {
		ri := agg[realmKeys[i]].info
		rj := agg[realmKeys[j]].info
		if ri.Region == rj.Region {
			return ri.Slug < rj.Slug
		}
		return ri.Region < rj.Region
	})

	realmsOut := make([]statusRealmEntry, 0, le
n(realmKeys))
	for _, key := range realmKeys {
		ra := agg[key]
		realmEntry := statusRealmEntry{
			Region:    ra.info.Region,
			RealmSlug: ra.info.Slug,
			RealmName: ra.info.Name,
		}
		var realmMissing, realmErrors int

		dungeonIDs := make([]int, 0, len(ra.dungeo
ns))
		for id := range ra.dungeons {
			dungeonIDs = append(dungeonIDs, id)
		}
		sort.Ints(dungeonIDs)

		for _, id := range dungeonIDs {
			dagg := ra.dungeons[id]
			sort.Ints(dagg.periods)
			sort.Ints(dagg.missing)
			sort.Ints(dagg.errors)

			status := coverageStatus(len(dagg.periods
), len(dagg.missing), len(dagg.errors))
			entry := statusDungeonEntry{
				DungeonID:    dagg.info.ID,
				DungeonSlug:  dagg.info.Slug,
				DungeonName:  dagg.info.Name,
				Status:       status,
				Periods:      append([]int(nil), dagg.pe
riods...),
				Missing:      append([]int(nil), dagg.mi
ssing...),
				ErrorPeriods: append([]int(nil), dagg.er
rors...),
			}
			realmEntry.Dungeons = append(realmEntry.D
ungeons, entry)
			realmEntry.TotalPeriods += len(dagg.perio
ds)
			realmMissing += len(dagg.missing)
			realmErrors += len(dagg.errors)
		}

		realmEntry.MissingPeriods = realmMissing
		realmEntry.ErrorPeriods = realmErrors
		realmEntry.Health = coverageStatus(realmEn
try.TotalPeriods, realmMissing, realmErrors)
		realmsOut = append(realmsOut, realmEntry)
	}

	generatedAt := time.Now().UTC().Format(time
.RFC3339)
	payload := statusPayload{
		GeneratedAt: generatedAt,
		Realms:      realmsOut,
	}

	if strings.TrimSpace(statusDir) == "" && st
rings.TrimSpace(outPath) == "" {
		return errors.New("status output path not 
specified")
	}

	if strings.TrimSpace(outPath) == "" {
		if err := os.MkdirAll(statusDir, 0o755); e
rr != nil {
			return fmt.Errorf("mkdir status dir: %w",
 err)
		}
		outPath = filepath.Join(statusDir, "latest
-runs.json")
	} else if err := os.MkdirAll(filepath.Dir(o
utPath), 0o755); err != nil {
		return fmt.Errorf("mkdir output dir: %w", 
err)
	}

	if err := writeStatusJSON(outPath, payload)
; err != nil {
		return err
	}

	if strings.TrimSpace(statusDir) != "" {
		for _, realm := range realmsOut {
			dir := filepath.Join(statusDir, realm.Reg
ion)
			if err := os.MkdirAll(dir, 0o755); err !=
 nil {
				return fmt.Errorf("mkdir realm dir: %w",
 err)
			}
			realmPath := filepath.Join(dir, fmt.Sprin
tf("%s.json", realm.RealmSlug))
			realmPayload := struct {
				GeneratedAt string           `json:"gene
rated_at"`
				Realm       statusRealmEntry `json:"real
m"`
			}{
				GeneratedAt: generatedAt,
				Realm:       realm,
			}
			if err := writeStatusJSON(realmPath, real
mPayload); err != nil {
				return err
			}
		}
		log.Info("wrote per-realm status files", "
dir", statusDir, "count", len(realmsOut))
	}

	log.Info("status coverage generated", "real
ms", len(realmsOut))
	return nil
}

func writeStatusJSON(path string, payload an
y) error {
	f, err := os.Create(path)
	if err != nil {
		return fmt.Errorf("create %s: %w", path, e
rr)
	}
	defer f.Close()

	enc := json.NewEncoder(f)
	enc.SetIndent("", "  ")
	if err := enc.Encode(payload); err != nil {
		return fmt.Errorf("encode %s: %w", path, e
rr)
	}
	return nil
}

func realmMapKey(region, slug string) string
 {
	return fmt.Sprintf("%s|%s", strings.ToLower
(region), strings.ToLower(slug))
}

func coverageStatus(total, missing, errors i
nt) string {
	if total == 0 {
		return "no_data"
	}
	deficit := missing + errors
	if deficit == 0 {
		return "ok"
	}
	if deficit >= total {
		return "no_data"
	}
	return "some_missing"
}
 
──────────────────────────────────────────────────────────────────────────────────────
OOKNET
────────────────────────────────────────────────
OOKNET