Space / database.go
Harp404's picture
partial
a8eed5c
Raw History Blame Contribute Delete
9.79 kB
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"math"
"net/http"
"os"
"time"
_ "github.com/lib/pq"
)
var db *sql.DB
// ---- Domain structs ----
type NodeState struct {
ID int `json:"id"`
Name string `json:"name"`
Online bool `json:"online"`
LogID int `json:"log_id"`
}
type Satellite struct {
ID int `json:"id"`
NoradID string `json:"norad_id"`
Name string `json:"name"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
AltKm float64 `json:"alt_km"`
Status string `json:"status"`
RiskScore float64 `json:"risk_score"`
Operator string `json:"operator"`
}
type Conjunction struct {
ID int `json:"id"`
Sat1ID int `json:"sat1_id"`
Sat2ID int `json:"sat2_id"`
Sat1Name string `json:"sat1_name"`
Sat2Name string `json:"sat2_name"`
TCA string `json:"tca"`
MinRangeKm float64 `json:"min_range_km"`
Probability float64 `json:"probability"`
RiskIndex float64 `json:"risk_index"`
Status string `json:"status"`
}
type NetworkState struct {
Nodes []NodeState `json:"nodes"`
Satellites []Satellite `json:"satellites"`
Conjunctions []Conjunction `json:"conjunctions"`
LeaderID int `json:"leader_id"`
}
// ---- Init ----
func initDB() {
dbURL := os.Getenv("DB_URL")
if dbURL == "" {
dbURL = "postgres://astromesh:astromesh@localhost:5432/astromesh?sslmode=disable"
}
var err error
for i := 0; i < 10; i++ {
db, err = sql.Open("postgres", dbURL)
if err == nil {
err = db.Ping()
}
if err == nil {
break
}
log.Printf("[DB] waiting for postgres (%d/10): %v", i+1, err)
time.Sleep(3 * time.Second)
}
if err != nil {
log.Fatalf("[DB] failed to connect: %v", err)
}
createTables()
seedConstellationData()
log.Println("[DB] ready")
}
func createTables() {
stmts := []string{
`CREATE TABLE IF NOT EXISTS nodes (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
zone TEXT NOT NULL
)`,
`CREATE TABLE IF NOT EXISTS satellites (
id SERIAL PRIMARY KEY,
norad_id TEXT NOT NULL,
name TEXT NOT NULL,
operator TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'NOMINAL',
risk_score FLOAT NOT NULL DEFAULT 0
)`,
`CREATE TABLE IF NOT EXISTS conjunctions (
id SERIAL PRIMARY KEY,
sat1_id INT NOT NULL,
sat2_id INT NOT NULL,
tca TIMESTAMPTZ NOT NULL,
min_range_km FLOAT NOT NULL,
probability FLOAT NOT NULL,
risk_index FLOAT NOT NULL,
status TEXT NOT NULL DEFAULT 'MONITORING',
locked_by INT
)`,
`CREATE TABLE IF NOT EXISTS maneuver_log (
id SERIAL PRIMARY KEY,
conjunction_id INT NOT NULL,
status TEXT NOT NULL,
source TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
}
for _, s := range stmts {
if _, err := db.Exec(s); err != nil {
log.Fatalf("[DB] createTables: %v", err)
}
}
}
// ---- Seeding ----
type seedSat struct {
noradID string
name string
operator string
lat, lon, alt float64
}
var seedSats = []seedSat{
{"25544", "ISS", "NASA", 51.6, -140.0, 408},
{"20580", "Hubble Space Telescope", "NASA", 28.5, -80.0, 547},
{"39084", "Starlink-1234", "SpaceX", 53.0, -70.0, 550},
{"39086", "Starlink-1235", "SpaceX", 53.2, -69.5, 550},
{"39087", "Starlink-1236", "SpaceX", 53.4, -69.0, 550},
{"38754", "Sentinel-2A", "ESA", 45.0, 10.0, 786},
{"33591", "NOAA-19", "NOAA", 38.0, -95.0, 870},
{"36411", "GOES-16", "NOAA", 0.0, -75.2, 35786},
{"32711", "GPS-IIF-3", "USAF", 55.0, 40.0, 20200},
{"24946", "Iridium-33 Debris", "Iridium", 86.0, 90.0, 776},
{"22675", "Cosmos-2251 Debris", "Russia", 74.0, 130.0, 780},
{"43013", "CARTOSAT-2F", "ISRO", 97.6, 77.0, 505},
{"39269", "RISAT-2", "ISRO", 41.0, 72.0, 550},
{"43685", "ALOS-2", "JAXA", 98.0, 142.0, 628},
{"28654", "Terra", "NASA", 98.2, -180.0, 705},
{"27424", "Aqua", "NASA", 98.2, 175.0, 705},
{"40697", "Sentinel-1A", "ESA", 98.2, 5.0, 693},
{"41335", "Sentinel-3A", "ESA", 98.6, 15.0, 814},
{"43566", "Yaogan-30", "CNSA", 35.0, 110.0, 600},
{"44387", "COSMO-SkyMed 4", "ASI", 97.9, 12.0, 619},
}
func seedConstellationData() {
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM satellites").Scan(&count); err != nil || count > 0 {
return
}
// Insert nodes
nodeNames := map[int]string{1: "ISRO", 2: "ESA", 3: "JAXA", 4: "SpaceX"}
nodeZones := map[int]string{1: "ASIA-SOUTH", 2: "EUROPE", 3: "ASIA-EAST", 4: "NORTH-AMERICA"}
for i := 1; i <= 4; i++ {
db.Exec("INSERT INTO nodes(id,name,zone) VALUES($1,$2,$3) ON CONFLICT DO NOTHING", i, nodeNames[i], nodeZones[i])
}
// Insert satellites
satIDs := make([]int, 0, len(seedSats))
for _, s := range seedSats {
var id int
err := db.QueryRow(
"INSERT INTO satellites(norad_id,name,operator,status,risk_score) VALUES($1,$2,$3,'NOMINAL',0) RETURNING id",
s.noradID, s.name, s.operator,
).Scan(&id)
if err != nil {
log.Printf("[DB] seed sat error: %v", err)
continue
}
satIDs = append(satIDs, id)
}
// Seed 8 conjunctions
type conjSeed struct {
s1, s2 int
rangeKm float64
prob float64
}
now := time.Now()
conjSeeds := []conjSeed{
{0, 10, 0.08, 0.120}, // ISS vs Iridium debris
{1, 10, 0.15, 0.090}, // Hubble vs Iridium debris
{2, 11, 0.22, 0.070}, // Starlink vs Cosmos debris
{3, 11, 0.45, 0.040}, // Starlink vs Cosmos debris
{5, 16, 0.80, 0.020}, // Sentinel-2A vs Sentinel-1A
{8, 9, 0.05, 0.150}, // GPS vs Iridium debris
{12, 13, 1.20, 0.008}, // RISAT vs ALOS
{14, 15, 1.80, 0.003}, // Terra vs Aqua
}
for i, c := range conjSeeds {
if c.s1 >= len(satIDs) || c.s2 >= len(satIDs) {
continue
}
riskIndex := c.prob * (1.0 / (c.rangeKm + 0.1)) * 1000
riskIndex = math.Min(riskIndex, 100.0)
tca := now.Add(time.Duration(i+1)*6*time.Hour)
_, err := db.Exec(
"INSERT INTO conjunctions(sat1_id,sat2_id,tca,min_range_km,probability,risk_index,status) VALUES($1,$2,$3,$4,$5,$6,'MONITORING')",
satIDs[c.s1], satIDs[c.s2], tca, c.rangeKm, c.prob, riskIndex,
)
if err != nil {
log.Printf("[DB] seed conjunction error: %v", err)
}
}
log.Println("[DB] seed complete")
}
// ---- Queries ----
func getNetworkState() NetworkState {
return NetworkState{
Nodes: getOnlineNodes(),
Satellites: getSatellites(),
Conjunctions: getConjunctions(),
LeaderID: getLeaderID(),
}
}
func getSatellites() []Satellite {
rows, err := db.Query("SELECT id,norad_id,name,operator,status,risk_score FROM satellites ORDER BY id")
if err != nil {
log.Printf("[DB] getSatellites: %v", err)
return nil
}
defer rows.Close()
// We need lat/lon/alt from the seed data (not stored in DB – derive from index)
var sats []Satellite
idx := 0
for rows.Next() {
var s Satellite
rows.Scan(&s.ID, &s.NoradID, &s.Name, &s.Operator, &s.Status, &s.RiskScore)
if idx < len(seedSats) {
s.Lat = seedSats[idx].lat
s.Lon = seedSats[idx].lon
s.AltKm = seedSats[idx].alt
}
idx++
sats = append(sats, s)
}
return sats
}
func getConjunctions() []Conjunction {
rows, err := db.Query(`
SELECT c.id, c.sat1_id, c.sat2_id,
s1.name, s2.name,
c.tca, c.min_range_km, c.probability, c.risk_index, c.status
FROM conjunctions c
JOIN satellites s1 ON s1.id = c.sat1_id
JOIN satellites s2 ON s2.id = c.sat2_id
ORDER BY c.risk_index DESC
`)
if err != nil {
log.Printf("[DB] getConjunctions: %v", err)
return nil
}
defer rows.Close()
var conj []Conjunction
for rows.Next() {
var c Conjunction
var tca time.Time
rows.Scan(&c.ID, &c.Sat1ID, &c.Sat2ID, &c.Sat1Name, &c.Sat2Name,
&tca, &c.MinRangeKm, &c.Probability, &c.RiskIndex, &c.Status)
c.TCA = tca.UTC().Format(time.RFC3339)
conj = append(conj, c)
}
return conj
}
func lockConjunction(id, nodeID int) bool {
res, err := db.Exec(
"UPDATE conjunctions SET locked_by=$1 WHERE id=$2 AND locked_by IS NULL",
nodeID, id,
)
if err != nil {
return false
}
n, _ := res.RowsAffected()
return n > 0
}
func unlockConjunction(id int) {
db.Exec("UPDATE conjunctions SET locked_by=NULL WHERE id=$1", id)
}
func updateConjunctionStatus(id int, status string) {
db.Exec("UPDATE conjunctions SET status=$1, locked_by=NULL WHERE id=$2", status, id)
}
func appendLog(action, detail string) int {
// Parse conjunction_id out of detail if possible, else 0
var logID int
db.QueryRow(
"INSERT INTO maneuver_log(conjunction_id,status,source) VALUES(0,$1,$2) RETURNING id",
action, detail,
).Scan(&logID)
return logID
}
func getReplSummary() map[string]interface{} {
nodes := getOnlineNodes()
result := make(map[string]interface{})
nodesMap := make(map[string]interface{})
for _, n := range nodes {
nodesMap[fmt.Sprintf("%d", n.ID)] = map[string]interface{}{
"nodeId": n.ID,
"lastLogId": n.LogID,
}
}
result["nodes"] = nodesMap
return result
}
// ---- HTTP handlers ----
func handleNetwork(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
state := getNetworkState()
json.NewEncoder(w).Encode(state)
}
func handleSatellites(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(getSatellites())
}
func handleConjunctions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(getConjunctions())
}
func handleReset(w http.ResponseWriter, r *http.Request) {
db.Exec("DELETE FROM maneuver_log")
db.Exec("UPDATE conjunctions SET status='MONITORING', locked_by=NULL")
db.Exec("UPDATE satellites SET risk_score=0, status='NOMINAL'")
broadcastNetworkUpdate()
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"ok":true}`)
}