chore: add reference docs, scripts, tests, demo, prototypes
- Reference docs: known issues, user journey, location redesign, mission merge plan - Scripts: fill_location_state, merge_missions, migrate_location, migrate_state - Tests: e2e test suite, asset entries unit tests - Demo: location board, hifi prototypes, test screenshots - Data backup: asset log entries and state nodes - Update .gitignore Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,202 @@
|
||||
"""
|
||||
Correctly fill location and state_label in state nodes based on semantic context.
|
||||
"""
|
||||
import os, psycopg, re
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
|
||||
conn = psycopg.connect(
|
||||
host=os.getenv("PGHOST"), port=os.getenv("PGPORT"),
|
||||
dbname=os.getenv("PGDATABASE"), user=os.getenv("PGUSER"),
|
||||
password=os.getenv("PGPASSWORD"))
|
||||
cur = conn.cursor()
|
||||
NOW = datetime.now(timezone.utc)
|
||||
|
||||
# ── Location inference ──
|
||||
# Map body name → orbit location, surface location (if applicable)
|
||||
BODY_LOC = {
|
||||
"mercury": ("Mercury Orbit", "Mercury Surface"),
|
||||
"venus": ("Venus Orbit", None), # no crewed surface ops
|
||||
"earth": ("LEO", "Earth Surface"),
|
||||
"moon": ("Lunar Orbit", "Lunar Surface"),
|
||||
"lunar": ("Lunar Orbit", "Lunar Surface"),
|
||||
"mars": ("Mars Orbit", "Mars Surface"),
|
||||
"phobos": ("Phobos Orbit", None),
|
||||
"deimos": ("Deimos Orbit", None),
|
||||
"ceres": ("Ceres Orbit", "Ceres Surface"),
|
||||
"vesta": ("Vesta Orbit", "Vesta Surface"),
|
||||
"jupiter": ("Jupiter System", None),
|
||||
"io": ("Io Orbit", "Io Surface"),
|
||||
"europa": ("Europa Orbit", "Europa Surface"),
|
||||
"ganymede": ("Ganymede Orbit", "Ganymede Surface"),
|
||||
"callisto": ("Callisto Orbit", "Callisto Surface"),
|
||||
"saturn": ("Saturn System", None),
|
||||
"titan": ("Titan Orbit", "Titan Surface"),
|
||||
"iapetus": ("Iapetus Orbit", "Iapetus Surface"),
|
||||
"enceladus": ("Enceladus Orbit", None),
|
||||
"dione": ("Dione Orbit", None),
|
||||
"rhea": ("Rhea Orbit", None),
|
||||
"tethys": ("Tethys Orbit", None),
|
||||
"mimas": ("Mimas Orbit", None),
|
||||
"uranus": ("Uranus System", None),
|
||||
"miranda": ("Miranda Orbit", None),
|
||||
"ariel": ("Ariel Orbit", None),
|
||||
"umbriel": ("Umbriel Orbit", None),
|
||||
"titania": ("Titania Orbit", None),
|
||||
"oberon": ("Oberon Orbit", None),
|
||||
"neptune": ("Neptune System", None),
|
||||
"triton": ("Triton Orbit", "Triton Surface"),
|
||||
"pluto": ("Pluto Orbit", "Pluto Surface"),
|
||||
"charon": ("Charon Orbit", None),
|
||||
"sun": ("Solar Orbit", None),
|
||||
"solar": ("Solar Orbit", None),
|
||||
"star port": ("LEO", None),
|
||||
"star ring": ("LEO", None),
|
||||
"leo": ("LEO", None),
|
||||
"geo": ("GEO", None),
|
||||
}
|
||||
|
||||
# Keywords that indicate surface ops vs orbital
|
||||
SURFACE_WORDS = {"surface", "landing", "descend", "edl", "eva", "base", "outpost",
|
||||
"crewed landing", "ground", "walk", "construction"}
|
||||
ORBIT_WORDS = {"orbit", "orbital", "arrival", "departure", "soi", "flyby",
|
||||
"survey", "parking", "dock", "rendezvous", "approach",
|
||||
"braking burn", "course correction", "insertion"}
|
||||
TRANSFER_WORDS = {"transfer", "transit", "outbound", "inbound", "return",
|
||||
"departure", "en route", "heading", "burn",
|
||||
"leave", "left", "sail"}
|
||||
|
||||
# State label inference
|
||||
STATE_KEYWORDS = [
|
||||
(["construction", "assembly", "assembl", "fitting", "install",
|
||||
"module delivery", "first module", "construction complete",
|
||||
"assembly halt", "assembly complete", "resume construction"], "Construction"),
|
||||
(["trial run", "testing", "test mission", "commissioning", "shakedown",
|
||||
"deep space test"], "Testing"),
|
||||
(["maintenance", "upgrade", "refit", "repair", "servicing",
|
||||
"parked to", "upgrade to"], "Maintenance"),
|
||||
(["departure", "depart", "leave", "outbound", "earth departure",
|
||||
"mars departure", "europa departure", "left", "en route to",
|
||||
"heading to"], "start-transit"),
|
||||
(["arrival", "arrive", "arrived", "return", "inbound",
|
||||
"braking burn", "course correction", "soi",
|
||||
"crew arrival", "crew return", "earth arrival",
|
||||
"mars arrival", "arriving"], "transit-complete"),
|
||||
(["exploration", "expedition", "surface operation", "surface expedition",
|
||||
"descend", "ascend", "eva operation", "orbital survey",
|
||||
"probe deployment", "atmosphere survey", "crewed landing",
|
||||
"lander", "surface ops", "rover", "observation"], "Exploration"),
|
||||
(["docked", "docking", "dock", "berthing", "mooring"], "Docked"),
|
||||
(["autonomous standby", "standby"], "Autonomous Standby"),
|
||||
(["crew handoff", "handoff", "handover"], "Crew Handoff"),
|
||||
]
|
||||
|
||||
def find_body(text):
|
||||
"""Find the primary celestial body mentioned in text (whole-word match only)."""
|
||||
if not text:
|
||||
return None
|
||||
text_lower = text.lower()
|
||||
# Sort by key length (longest first) to match "star port" before "star"
|
||||
for body in sorted(BODY_LOC.keys(), key=len, reverse=True):
|
||||
pattern = r'\b' + re.escape(body) + r'\b'
|
||||
if re.search(pattern, text_lower):
|
||||
return body
|
||||
return None
|
||||
|
||||
def infer_location(node_title, node_detail, entry_title, mission_label, asset_name, asset_type, home_region):
|
||||
"""Infer the correct location for a state node."""
|
||||
node_text = f"{node_title or ''} {node_detail or ''}".lower()
|
||||
full_text = f"{entry_title or ''} {mission_label or ''}".lower()
|
||||
|
||||
# Priority 1: body mentioned in node title/detail
|
||||
body = find_body(node_text)
|
||||
if body and body in BODY_LOC:
|
||||
orbit_loc, surf_loc = BODY_LOC[body]
|
||||
has_surface = any(w in node_text for w in SURFACE_WORDS)
|
||||
has_orbit = any(w in node_text for w in ORBIT_WORDS)
|
||||
if surf_loc and has_surface and not has_orbit:
|
||||
return surf_loc
|
||||
return orbit_loc
|
||||
|
||||
# Priority 2: body in entry title / mission label
|
||||
body = find_body(full_text)
|
||||
if body and body in BODY_LOC:
|
||||
orbit_loc, surf_loc = BODY_LOC[body]
|
||||
has_surface = any(w in node_text for w in SURFACE_WORDS)
|
||||
has_orbit = any(w in node_text for w in ORBIT_WORDS)
|
||||
if surf_loc and has_surface:
|
||||
return surf_loc
|
||||
return orbit_loc
|
||||
|
||||
# Priority 3: transfer keywords
|
||||
all_text = f"{node_text} {full_text}"
|
||||
if any(w in all_text for w in TRANSFER_WORDS):
|
||||
if body:
|
||||
return BODY_LOC.get(body, (None, None))[0] or "Transfer"
|
||||
return "Transfer"
|
||||
|
||||
# Priority 4: home region or asset type default
|
||||
if home_region:
|
||||
hr = str(home_region).lower()
|
||||
for b in BODY_LOC:
|
||||
if b in hr:
|
||||
return BODY_LOC[b][0]
|
||||
return "LEO"
|
||||
|
||||
def infer_state(node_title, node_detail, entry_title, mission_label):
|
||||
"""Infer the correct state_label for a state node."""
|
||||
context = f"{node_title or ''} {node_detail or ''} {entry_title or ''} {mission_label or ''}".lower()
|
||||
for keywords, label in STATE_KEYWORDS:
|
||||
for kw in keywords:
|
||||
if kw in context:
|
||||
return label
|
||||
return None
|
||||
|
||||
# ── Main ──
|
||||
cur.execute("""
|
||||
SELECT sn.id::text, sn.title, sn.detail, sn.at_time,
|
||||
le.title as entry_title, le.mission_label,
|
||||
a.name as asset_name, a.asset_type, a.home_region
|
||||
FROM asset_state_nodes sn
|
||||
JOIN asset_log_entries le ON sn.entry_id = le.id
|
||||
JOIN assets a ON le.asset_id = a.id
|
||||
ORDER BY a.name, sn.at_time
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
|
||||
updated = 0
|
||||
for r in rows:
|
||||
nid, ntitle, ndetail, atime, entry_title, mission, asset_name, asset_type, home_region = r
|
||||
|
||||
new_loc = infer_location(ntitle, ndetail, entry_title, mission, asset_name, asset_type, home_region)
|
||||
new_st = infer_state(ntitle, ndetail, entry_title, mission)
|
||||
|
||||
# Only update if different from current
|
||||
cur.execute("SELECT location, state_label FROM asset_state_nodes WHERE id = %s", (nid,))
|
||||
cur_loc, cur_st = cur.fetchone()
|
||||
|
||||
if (new_loc and new_loc != cur_loc) or (new_st and new_st != cur_st):
|
||||
cur.execute("""
|
||||
UPDATE asset_state_nodes
|
||||
SET location = COALESCE(%s, location),
|
||||
state_label = COALESCE(%s, state_label),
|
||||
updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (new_loc, new_st, NOW, nid))
|
||||
updated += 1
|
||||
print(f" {asset_name[:20]:20s} | {str(ntitle)[:45]:45s} | {cur_loc or 'NULL':20s} → {new_loc or 'NULL':20s} | {cur_st or 'NULL':15s} → {new_st or 'NULL':15s}")
|
||||
|
||||
conn.commit()
|
||||
|
||||
cur.execute("""
|
||||
SELECT COUNT(*),
|
||||
COUNT(CASE WHEN location IS NOT NULL AND location!='' THEN 1 END),
|
||||
COUNT(CASE WHEN state_label IS NOT NULL AND state_label!='' THEN 1 END)
|
||||
FROM asset_state_nodes
|
||||
""")
|
||||
r = cur.fetchone()
|
||||
print(f"\nFinal: {r[0]} total, {r[1]} with location, {r[2]} with state_label")
|
||||
print(f"Updated: {updated} nodes")
|
||||
conn.close()
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Merge Outbound/Operations/Inbound entries into single mission entries.
|
||||
Stage 1: Add missing Inbound state nodes
|
||||
Stage 2: Merge entries
|
||||
|
||||
BACKUP already created in data_backup/ before running this script.
|
||||
"""
|
||||
import os, psycopg, uuid
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
|
||||
conn = psycopg.connect(
|
||||
host=os.getenv("PGHOST"), port=os.getenv("PGPORT"),
|
||||
dbname=os.getenv("PGDATABASE"), user=os.getenv("PGUSER"),
|
||||
password=os.getenv("PGPASSWORD"))
|
||||
cur = conn.cursor()
|
||||
tz = timezone.utc
|
||||
NOW = datetime.now(tz)
|
||||
|
||||
def ts(y,m,d,h=0,mi=0,s=0):
|
||||
return datetime(y,m,d,h,mi,s,tzinfo=tz)
|
||||
|
||||
def add_node(entry_id, title, detail, at_time, seq=None):
|
||||
"""Add a state node to an entry."""
|
||||
cur.execute("""
|
||||
INSERT INTO asset_state_nodes (id, entry_id, title, detail, at_time, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
""", (str(uuid.uuid4()), entry_id, title, detail, at_time, NOW, NOW))
|
||||
|
||||
def move_nodes(from_entry_id, to_entry_id):
|
||||
"""Move all state nodes from one entry to another."""
|
||||
cur.execute("""
|
||||
UPDATE asset_state_nodes SET entry_id = %s, updated_at = %s
|
||||
WHERE entry_id = %s
|
||||
""", (to_entry_id, NOW, from_entry_id))
|
||||
return cur.rowcount
|
||||
|
||||
def merge_group(ops_id, outbound_ids, inbound_ids, new_start, new_end, new_title, new_state_label, new_location):
|
||||
"""
|
||||
Merge outbound + ops + inbound entries.
|
||||
- Extend ops entry start/end
|
||||
- Move all state nodes from outbound & inbound to ops
|
||||
- Delete outbound & inbound entries
|
||||
"""
|
||||
# Extend ops
|
||||
cur.execute("""
|
||||
UPDATE asset_log_entries SET start_at = %s, end_at = %s,
|
||||
title = %s, state_label = %s, location = %s, updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (new_start, new_end, new_title, new_state_label, new_location, NOW, ops_id))
|
||||
|
||||
deleted = []
|
||||
total_moved = 0
|
||||
for eid in outbound_ids + inbound_ids:
|
||||
n = move_nodes(eid, ops_id)
|
||||
total_moved += n
|
||||
cur.execute("DELETE FROM asset_log_entries WHERE id = %s", (eid,))
|
||||
deleted.append(eid[:8])
|
||||
print(f" Merged: ops={ops_id[:8]} start={new_start} end={new_end} moved={total_moved} nodes, deleted={deleted}")
|
||||
return total_moved
|
||||
|
||||
print("=" * 60)
|
||||
print("STAGE 1: Adding missing Inbound state nodes")
|
||||
print("=" * 60)
|
||||
|
||||
# --- EE-1 (XH-01) Inbound: 92315017 ---
|
||||
# User provided: 2059-07-10 Europa Departure, 2059-11-15 Earth Arrival, 2059-11-16 crew return
|
||||
add_node('92315017-7011-4930-aabb-d00a6f3f4994',
|
||||
'Europa Departure', 'Uses standard trajectory with 112km/s of departure burn',
|
||||
ts(2059,7,10,8,0))
|
||||
add_node('92315017-7011-4930-aabb-d00a6f3f4994',
|
||||
'Earth Arrival', 'Parking to Star Port for the first time',
|
||||
ts(2059,11,15,8,0))
|
||||
add_node('92315017-7011-4930-aabb-d00a6f3f4994',
|
||||
'Europa Expedition 1 crew return',
|
||||
'Crew disembark at Star Port after 15-month Jupiter system expedition',
|
||||
ts(2059,11,16,8,0))
|
||||
print("EE-1 (XH-01): 3 state nodes added")
|
||||
|
||||
# --- EOCM-2 (ST-02) Inbound: efdb28dd ---
|
||||
add_node('efdb28dd-3e02-4b16-80e8-7a2d4df76825',
|
||||
'Europa Departure',
|
||||
'Departure from Europa after completing outpost construction Phase 2',
|
||||
ts(2058,9,6,8,0))
|
||||
add_node('efdb28dd-3e02-4b16-80e8-7a2d4df76825',
|
||||
'Earth Arrival',
|
||||
'Parking to Star Port',
|
||||
ts(2058,11,8,8,0))
|
||||
add_node('efdb28dd-3e02-4b16-80e8-7a2d4df76825',
|
||||
'Europa Outpost Construction Mission 2 crew return',
|
||||
'Crew disembark at Star Port',
|
||||
ts(2058,11,9,8,0))
|
||||
print("EOCM-2 (ST-02): 3 state nodes added")
|
||||
|
||||
# --- AEM (XH-02) Inbound: d74dfb61 ---
|
||||
add_node('d74dfb61-fd3a-492c-b019-253ec9cd826d',
|
||||
'Vesta Departure',
|
||||
'Departure from Vesta after completing asteroid survey operations',
|
||||
ts(2058,1,4,8,0))
|
||||
add_node('d74dfb61-fd3a-492c-b019-253ec9cd826d',
|
||||
'Earth Arrival',
|
||||
'Parking to Star Port',
|
||||
ts(2058,4,17,8,0))
|
||||
add_node('d74dfb61-fd3a-492c-b019-253ec9cd826d',
|
||||
'Asteroid Exploration Mission crew return',
|
||||
'Crew disembark at Star Port after Vesta exploration',
|
||||
ts(2058,4,18,8,0))
|
||||
print("AEM (XH-02): 3 state nodes added")
|
||||
|
||||
# --- ME-3 crewed Inbound (XH-02): 33a45210 ---
|
||||
add_node('33a45210-3497-4826-ba6e-291b384ebe7e',
|
||||
'Mars Departure',
|
||||
'Departure from Mars with ME-3 crew returning to Earth',
|
||||
ts(2059,11,4,8,0))
|
||||
add_node('33a45210-3497-4826-ba6e-291b384ebe7e',
|
||||
'Earth Arrival',
|
||||
'Parking to Star Port',
|
||||
ts(2060,1,6,8,0))
|
||||
add_node('33a45210-3497-4826-ba6e-291b384ebe7e',
|
||||
'Mars Expedition 3 crew return',
|
||||
'Crew disembark at Star Port after extended Mars surface expedition',
|
||||
ts(2060,1,7,8,0))
|
||||
print("ME-3 crewed (XH-02): 3 state nodes added")
|
||||
|
||||
# --- ME-3 uncrewed Inbound (XH-02): f43aa249 ---
|
||||
add_node('f43aa249-80d1-426c-9aca-a341411be98d',
|
||||
'Mars Departure (uncrewed)',
|
||||
'XH-02 returns to Earth uncrewed; ME-3 crew remains at Mars for ME-4 handoff',
|
||||
ts(2059,5,4,8,0))
|
||||
add_node('f43aa249-80d1-426c-9aca-a341411be98d',
|
||||
'Earth Arrival (uncrewed)',
|
||||
'Uncrewed parking to Star Port for maintenance rotation',
|
||||
ts(2059,7,6,8,0))
|
||||
print("ME-3 uncrewed (XH-02): 2 state nodes added")
|
||||
|
||||
# Commit stage 1
|
||||
conn.commit()
|
||||
print("\nStage 1 complete. Starting Stage 2...\n")
|
||||
|
||||
print("=" * 60)
|
||||
print("STAGE 2: Merging mission entries")
|
||||
print("=" * 60)
|
||||
|
||||
total_deleted = 0
|
||||
total_moved = 0
|
||||
|
||||
# === XH-01 ===
|
||||
print("\n--- XH-01 羲和号 ---")
|
||||
|
||||
# MMD
|
||||
n = merge_group(
|
||||
'23b4304f-8fca-4d78-8a93-c9f78fab51e0', # Ops
|
||||
['e54817e0-6809-404d-be62-c6017d143362'], # Outbound
|
||||
['ded06cd6-f9a4-4646-9be4-2f01cf067334'], # Inbound
|
||||
ts(2054,7,10,8,0), ts(2055,2,28,8,0),
|
||||
'Mars Mission Demo', 'Transfer; Planetary Operations; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# MOCM-1
|
||||
n = merge_group(
|
||||
'ec34d68b-4728-42eb-af79-b3e8805aa253',
|
||||
['aab3125c-c99e-4af5-ab64-ffe7fda48ad7'],
|
||||
['bdf99335-d329-4bd2-8e17-27ac1f097ca9'],
|
||||
ts(2055,6,15,8,0), ts(2056,3,18,8,0),
|
||||
'Mars One Base Construction Mission 1', 'Transfer; Base Construction; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# MOCM-3
|
||||
n = merge_group(
|
||||
'fd22c4b2-4938-4c01-ae92-e84f14d8b8dd',
|
||||
['ea2024c5-95e6-4acc-a88d-998183a35a5a'],
|
||||
['e5f549b9-0446-48be-a606-d9c1ada664b7'],
|
||||
ts(2056,7,30,8,0), ts(2057,3,25,8,0),
|
||||
'Mars One Base Construction Mission 3', 'Transfer; Base Construction; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# ME-2
|
||||
n = merge_group(
|
||||
'7f5da9b3-94fa-4bd5-a6bd-94f6841c71a2',
|
||||
['30c84287-e5d1-473c-93fb-9d36d426516e'],
|
||||
['058bbf6d-5c51-49c4-8516-8b6931db53c1'],
|
||||
ts(2057,5,5,8,0), ts(2058,5,20,8,0),
|
||||
'Mars Expedition 2', 'Transfer; Planetary Operations; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# EE-1
|
||||
n = merge_group(
|
||||
'f01bfb99-f981-45f6-808b-7c5222503715',
|
||||
['4ff368c4-503b-42ce-9e82-b4da291d3203'],
|
||||
['92315017-7011-4930-aabb-d00a6f3f4994'],
|
||||
ts(2058,7,10,8,0), ts(2059,11,15,8,0),
|
||||
'Europa Expedition 1', 'Transfer; Jupiter Exploration; Transfer', 'Jupiter System')
|
||||
total_moved += n
|
||||
|
||||
# EE-3 (ongoing, no inbound)
|
||||
cur.execute("""
|
||||
UPDATE asset_log_entries SET start_at = %s, title = %s,
|
||||
state_label = %s, updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (ts(2060,2,20,8,0), 'Europa Expedition 3',
|
||||
'Transfer; Jupiter Exploration', NOW,
|
||||
'91d4948a-ae04-43a8-ac3b-def9caecc8e5'))
|
||||
n = move_nodes('ee52cc53-7d9c-4134-ba1b-4f7cfb8fdce9', '91d4948a-ae04-43a8-ac3b-def9caecc8e5')
|
||||
cur.execute("DELETE FROM asset_log_entries WHERE id = 'ee52cc53-7d9c-4134-ba1b-4f7cfb8fdce9'")
|
||||
print(f" Merged (ongoing): ops=91d4948a start=2060-02-20 end=ongoing moved={n} nodes")
|
||||
|
||||
# === ST-01 ===
|
||||
print("\n--- ST-01 万星源号 ---")
|
||||
|
||||
# ISSEM-1 (5 entries → 1)
|
||||
# Keep Venus Ops as main (longest ops span), merge Mercury Transfer + Mercury Ops + Venus Transfer + Inbound
|
||||
n1 = move_nodes('4bfddc84-dfec-43e3-b534-4fb798bcc73f', 'f28469ee-1dbc-4232-98ff-91f7abcb9d59') # Mercury Transfer → Venus Ops
|
||||
n2 = move_nodes('42698c8c-a1cc-4742-9885-7b9e650f9867', 'f28469ee-1dbc-4232-98ff-91f7abcb9d59') # Mercury Ops → Venus Ops
|
||||
n3 = move_nodes('2adfa016-b23a-4459-af6e-a84da2aa2469', 'f28469ee-1dbc-4232-98ff-91f7abcb9d59') # Venus Transfer → Venus Ops
|
||||
n4 = move_nodes('1f6e85aa-121a-4f47-95b6-35f040da055c', 'f28469ee-1dbc-4232-98ff-91f7abcb9d59') # Inbound → Venus Ops
|
||||
cur.execute("""
|
||||
UPDATE asset_log_entries SET start_at = %s, end_at = %s, title = %s,
|
||||
state_label = %s, location = %s, updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (ts(2055,3,20,8,0), ts(2056,4,12,8,0),
|
||||
'Inner Solar System Exploration Mission 1',
|
||||
'Transfer; Planetary Operations; Transfer', 'Mercury; Venus',
|
||||
NOW, 'f28469ee-1dbc-4232-98ff-91f7abcb9d59'))
|
||||
for eid in ['4bfddc84-dfec-43e3-b534-4fb798bcc73f','42698c8c-a1cc-4742-9885-7b9e650f9867',
|
||||
'2adfa016-b23a-4459-af6e-a84da2aa2469','1f6e85aa-121a-4f47-95b6-35f040da055c']:
|
||||
cur.execute("DELETE FROM asset_log_entries WHERE id = %s", (eid,))
|
||||
print(f" Merged ISSEM-1 (5→1): moved={n1+n2+n3+n4} nodes, deleted 4 entries")
|
||||
|
||||
# MOCM-2
|
||||
n = merge_group(
|
||||
'12059276-d10a-4a37-9b8c-6a6183768504',
|
||||
['ffd625b0-0de4-4f47-8c8d-36669a1675f6'],
|
||||
['cf351aa5-4149-4938-939b-edcb846f4b14'],
|
||||
ts(2056,6,2,8,0), ts(2056,11,3,8,0),
|
||||
'Mars One Construction Mission 2', 'Transfer; Base Construction; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# EOCM-1
|
||||
n = merge_group(
|
||||
'545f687e-bd2c-409a-8fb8-186a6dead67e',
|
||||
['7747339c-f602-4ddf-8511-6862006f789b'],
|
||||
['43f2ece9-283b-4484-b838-d684b9ada4d6'],
|
||||
ts(2056,12,15,8,0), ts(2057,12,20,8,0),
|
||||
'Europa Outpost Construction Mission 1', 'Transfer; Outpost Construction; Transfer', 'Europa')
|
||||
total_moved += n
|
||||
|
||||
# SEM-1
|
||||
n = merge_group(
|
||||
'022b0296-5c1a-41f8-8634-97dc663b05d9',
|
||||
['cad8537e-1966-4b3c-abed-35c511950ac8'],
|
||||
['f536e950-a024-45e3-95ff-da5f77772119'],
|
||||
ts(2058,6,1,8,0), ts(2059,11,24,8,0),
|
||||
'Saturn Exploration Mission 1', 'Transfer; Planetary Operations; Transfer', 'Saturn System')
|
||||
total_moved += n
|
||||
|
||||
# OSSEM-1 (ongoing)
|
||||
cur.execute("""
|
||||
UPDATE asset_log_entries SET start_at = %s, title = %s,
|
||||
state_label = %s, updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (ts(2060,2,11,8,0), 'Outer Solar System Exploration Mission 1',
|
||||
'Transfer; Exploration', NOW,
|
||||
'18b3429e-28d3-489c-b65c-35e0e9775a0d'))
|
||||
n = move_nodes('8a9de074-6348-4190-bd27-e3bdeddb70f7', '18b3429e-28d3-489c-b65c-35e0e9775a0d')
|
||||
cur.execute("DELETE FROM asset_log_entries WHERE id = '8a9de074-6348-4190-bd27-e3bdeddb70f7'")
|
||||
print(f" Merged OSSEM-1 (ongoing): moved={n} nodes")
|
||||
|
||||
# === ST-02 ===
|
||||
print("\n--- ST-02 克里斯滕号 ---")
|
||||
|
||||
# EOCM-2
|
||||
n = merge_group(
|
||||
'fb17cab1-dcbc-4814-83e9-5fb81a73f869',
|
||||
['a61ee6fc-5495-4b5b-a505-968669b81b18'],
|
||||
['efdb28dd-3e02-4b16-80e8-7a2d4df76825'],
|
||||
ts(2057,10,6,8,0), ts(2058,11,8,8,0),
|
||||
'Europa Outpost Construction Mission 2', 'Transfer; Outpost Construction; Transfer', 'Europa')
|
||||
total_moved += n
|
||||
|
||||
# EE-2
|
||||
n = merge_group(
|
||||
'65910288-0c2a-4800-bfce-39495efafce2',
|
||||
['fe21560f-60f4-48bb-bbdb-e2c1ab6fe3af'],
|
||||
['e77cd6b5-a2e2-479b-817b-67c1c59bfdee'],
|
||||
ts(2059,4,6,8,0), ts(2060,8,30,6,11),
|
||||
'Europa Expedition 2', 'Transfer; Planetary Operations; Transfer', 'Europa')
|
||||
total_moved += n
|
||||
|
||||
# === XH-02 ===
|
||||
print("\n--- XH-02 太白号 ---")
|
||||
|
||||
# AEM
|
||||
n = merge_group(
|
||||
'99eb75ba-e02b-4cdc-a5f1-3ea547cc7d4a',
|
||||
['7c166ff7-df3b-495d-a7df-9b99949e23f0'],
|
||||
['d74dfb61-fd3a-492c-b019-253ec9cd826d'],
|
||||
ts(2057,5,4,8,0), ts(2058,4,17,8,0),
|
||||
'Asteroid Exploration Mission', 'Transfer; Asteroid Operations; Transfer', 'Vesta')
|
||||
total_moved += n
|
||||
|
||||
# ME-3 (merge Ops + Uncrewed Inbound + Crewed Inbound)
|
||||
n1 = move_nodes('f43aa249-80d1-426c-9aca-a341411be98d', '956cc319-668e-45d1-ab7c-9f0aa408c5f7')
|
||||
n2 = move_nodes('33a45210-3497-4826-ba6e-291b384ebe7e', '956cc319-668e-45d1-ab7c-9f0aa408c5f7')
|
||||
cur.execute("""
|
||||
UPDATE asset_log_entries SET start_at = %s, end_at = %s, title = %s,
|
||||
state_label = %s, location = %s, updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (ts(2058,4,17,8,0), ts(2060,1,6,8,0),
|
||||
'Mars Expedition 3', 'Transfer; Planetary Operations; Transfer', 'Mars',
|
||||
NOW, '956cc319-668e-45d1-ab7c-9f0aa408c5f7'))
|
||||
for eid in ['f43aa249-80d1-426c-9aca-a341411be98d','33a45210-3497-4826-ba6e-291b384ebe7e']:
|
||||
cur.execute("DELETE FROM asset_log_entries WHERE id = %s", (eid,))
|
||||
print(f" Merged ME-3 (3→1): moved={n1+n2} nodes, deleted 2 entries")
|
||||
total_moved += n1 + n2
|
||||
|
||||
# ME-4 — SKIP (单独处理)
|
||||
print(" ME-4: SKIPPED (单独处理)")
|
||||
|
||||
# === JW-01 ===
|
||||
print("\n--- JW-01 金乌号 ---")
|
||||
n = merge_group(
|
||||
'30930d1b-d32b-4ef6-97ae-6d11c777ad53',
|
||||
['b99062ea-4547-4cdc-8bf1-3b1f0c28a4be'],
|
||||
['00c8b5c6-e363-4aee-a768-2fc3fa7a0138'],
|
||||
ts(2059,8,6,8,0), ts(2060,10,26,10,3),
|
||||
'Mars One Expansion', 'Transfer; Cargo Delivery; Transfer', 'Mars')
|
||||
total_moved += n
|
||||
|
||||
# === Solar Shuttle 2 ===
|
||||
print("\n--- Solar Shuttle 2 ---")
|
||||
n = merge_group(
|
||||
'c0271f19-e604-4c15-9c00-7118fec1928e',
|
||||
['5a2092a8-ddad-41b3-9819-fa52557332ed'],
|
||||
['687615b6-144f-4fd5-b3ee-709e4eaea6aa'],
|
||||
ts(2059,12,9,8,0), ts(2060,6,26,8,0),
|
||||
'Europa Outpost Expansion Mission', 'Transfer; Outpost Expansion; Transfer', 'Europa')
|
||||
total_moved += n
|
||||
|
||||
# === Commit ===
|
||||
conn.commit()
|
||||
print(f"\n{'='*60}")
|
||||
print(f"COMPLETE. Total state nodes moved: {total_moved}")
|
||||
print(f"{'='*60}")
|
||||
|
||||
# Verify
|
||||
cur.execute("""
|
||||
SELECT a.name, COUNT(*) as entries, COUNT(le.mission_label) as with_mission
|
||||
FROM asset_log_entries le JOIN assets a ON le.asset_id = a.id
|
||||
WHERE a.name ILIKE '%XH-01%' OR a.name ILIKE '%XH-02%' OR a.name ILIKE '%ST-01%'
|
||||
OR a.name ILIKE '%ST-02%' OR a.name ILIKE '%JW-01%' OR a.name ILIKE '%Solar Shuttle 2%'
|
||||
GROUP BY a.name ORDER BY a.name
|
||||
""")
|
||||
print("\nPost-merge entry counts:")
|
||||
for r in cur.fetchall():
|
||||
print(f" {r[0]:20s}: {r[1]} entries")
|
||||
|
||||
# Count state nodes per asset
|
||||
cur.execute("""
|
||||
SELECT a.name, COUNT(sn.id) as nodes
|
||||
FROM asset_state_nodes sn
|
||||
JOIN asset_log_entries le ON sn.entry_id = le.id
|
||||
JOIN assets a ON le.asset_id = a.id
|
||||
WHERE a.name ILIKE '%XH-01%' OR a.name ILIKE '%XH-02%' OR a.name ILIKE '%ST-01%'
|
||||
OR a.name ILIKE '%ST-02%' OR a.name ILIKE '%JW-01%' OR a.name ILIKE '%Solar Shuttle 2%'
|
||||
GROUP BY a.name ORDER BY a.name
|
||||
""")
|
||||
print("\nPost-merge state node counts (should match pre-merge total):")
|
||||
for r in cur.fetchall():
|
||||
print(f" {r[0]:20s}: {r[1]} nodes")
|
||||
|
||||
conn.close()
|
||||
print("\nDone.")
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
Migrate location and state_label from asset_log_entries to asset_state_nodes.
|
||||
1. Add columns to asset_state_nodes
|
||||
2. For each interval: migrate location/state_label to first state node (or create one)
|
||||
3. Drop columns from asset_log_entries
|
||||
"""
|
||||
import os, psycopg, uuid
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
|
||||
conn = psycopg.connect(
|
||||
host=os.getenv("PGHOST"), port=os.getenv("PGPORT"),
|
||||
dbname=os.getenv("PGDATABASE"), user=os.getenv("PGUSER"),
|
||||
password=os.getenv("PGPASSWORD"))
|
||||
cur = conn.cursor()
|
||||
tz = timezone.utc
|
||||
NOW = datetime.now(tz)
|
||||
|
||||
# Step 1: Add columns
|
||||
print("Step 1: Adding location and state_label to asset_state_nodes...")
|
||||
try:
|
||||
cur.execute("ALTER TABLE asset_state_nodes ADD COLUMN location VARCHAR(255)")
|
||||
print(" Added location column")
|
||||
except Exception as e:
|
||||
if 'already exists' in str(e):
|
||||
print(" location column already exists")
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
cur.execute("ALTER TABLE asset_state_nodes ADD COLUMN state_label VARCHAR(120)")
|
||||
print(" Added state_label column")
|
||||
except Exception as e:
|
||||
if 'already exists' in str(e):
|
||||
print(" state_label column already exists")
|
||||
else:
|
||||
raise
|
||||
|
||||
# Step 2: Migrate data
|
||||
print("\nStep 2: Migrating location and state_label to state nodes...")
|
||||
cur.execute("""
|
||||
SELECT le.id, le.location, le.state_label, le.title, le.start_at,
|
||||
(SELECT COUNT(*) FROM asset_state_nodes sn WHERE sn.entry_id = le.id) as nc
|
||||
FROM asset_log_entries le
|
||||
WHERE le.location IS NOT NULL OR le.state_label IS NOT NULL
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
print(f" Found {len(rows)} entries with location/state_label")
|
||||
|
||||
created = 0
|
||||
updated = 0
|
||||
for row in rows:
|
||||
entry_id, loc, st_lbl, title, start_at, nc = row
|
||||
if nc > 0:
|
||||
# Update the first (earliest) state node of this interval
|
||||
cur.execute("""
|
||||
UPDATE asset_state_nodes
|
||||
SET location = COALESCE(%s, location),
|
||||
state_label = COALESCE(%s, state_label),
|
||||
updated_at = %s
|
||||
WHERE id = (
|
||||
SELECT id FROM asset_state_nodes
|
||||
WHERE entry_id = %s
|
||||
ORDER BY at_time ASC LIMIT 1
|
||||
)
|
||||
""", (loc, st_lbl, NOW, entry_id))
|
||||
updated += 1
|
||||
else:
|
||||
# No state nodes — create one at the interval's start_at
|
||||
cur.execute("""
|
||||
INSERT INTO asset_state_nodes (id, entry_id, title, detail, at_time, location, state_label, created_at, updated_at)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""", (str(uuid.uuid4()), entry_id, title or '(Start)',
|
||||
f'Auto-created during location migration. Original interval: {title}',
|
||||
start_at, loc, st_lbl, NOW, NOW))
|
||||
created += 1
|
||||
print(f" Updated {updated} existing state nodes, created {created} new ones")
|
||||
|
||||
# Step 3: Drop columns
|
||||
print("\nStep 3: Dropping location and state_label from asset_log_entries...")
|
||||
cur.execute("ALTER TABLE asset_log_entries DROP COLUMN location")
|
||||
print(" Dropped location")
|
||||
cur.execute("ALTER TABLE asset_log_entries DROP COLUMN state_label")
|
||||
print(" Dropped state_label")
|
||||
|
||||
conn.commit()
|
||||
print("\nMigration complete.")
|
||||
conn.close()
|
||||
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Migrate state labels: Transfer/In Transit -> start-transit / transit-complete.
|
||||
Set previous_location and transit_location on start-transit nodes.
|
||||
"""
|
||||
import os, psycopg, re
|
||||
from datetime import datetime, timezone
|
||||
from dotenv import load_dotenv
|
||||
from pathlib import Path
|
||||
|
||||
load_dotenv(Path(__file__).resolve().parents[1] / ".env")
|
||||
conn = psycopg.connect(
|
||||
host=os.getenv("PGHOST"), port=os.getenv("PGPORT"),
|
||||
dbname=os.getenv("PGDATABASE"), user=os.getenv("PGUSER"),
|
||||
password=os.getenv("PGPASSWORD"))
|
||||
cur = conn.cursor()
|
||||
NOW = datetime.now(timezone.utc)
|
||||
|
||||
# 1. Fetch all state nodes with their parent entry info, ordered by time
|
||||
cur.execute("""
|
||||
SELECT sn.id::text, sn.title, sn.detail, sn.at_time, sn.location, sn.state_label,
|
||||
le.title as entry_title, le.mission_label, le.start_at, le.end_at,
|
||||
a.name as asset_name
|
||||
FROM asset_state_nodes sn
|
||||
JOIN asset_log_entries le ON sn.entry_id = le.id
|
||||
JOIN assets a ON le.asset_id = a.id
|
||||
ORDER BY a.name, le.start_at, sn.at_time
|
||||
""")
|
||||
rows = cur.fetchall()
|
||||
print(f"Processing {len(rows)} state nodes")
|
||||
|
||||
# Group state nodes by entry
|
||||
from collections import defaultdict
|
||||
entries = defaultdict(list)
|
||||
for r in rows:
|
||||
eid = (r[6], r[7], r[8]) # entry_title, mission_label, start_at
|
||||
entries[eid].append(r)
|
||||
|
||||
updated = 0
|
||||
|
||||
for ekey, nodes in entries.items():
|
||||
entry_title, mission_label, entry_start = ekey
|
||||
|
||||
# Find pairs: departure -> arrival within this entry
|
||||
# A departure has keywords: depart, departure, leave, outbound
|
||||
# An arrival has keywords: arriv, arrival, inbound, return
|
||||
|
||||
for i, node in enumerate(nodes):
|
||||
nid, ntitle, ndetail, ntime, nloc, nst, *_ = node
|
||||
title_lower = (ntitle or "").lower()
|
||||
detail_lower = (ndetail or "").lower()
|
||||
ctx = f"{title_lower} {detail_lower}"
|
||||
|
||||
new_st = None
|
||||
new_prev_loc = None
|
||||
new_transit_loc = None
|
||||
|
||||
is_departure = any(w in ctx for w in
|
||||
["departure", "depart", "leave", "outbound", "departed"])
|
||||
is_arrival = any(w in ctx for w in
|
||||
["arrival", "arriv", "arrive", "inbound", "return", "soi"])
|
||||
|
||||
if is_departure and is_arrival:
|
||||
# Ambiguous - use position context. If it mentions Earth or a body + departure, it's departure
|
||||
if "departure" in ctx or "depart" in ctx or "leave" in ctx:
|
||||
is_arrival = False
|
||||
else:
|
||||
is_departure = False
|
||||
|
||||
if is_departure:
|
||||
new_st = "start-transit"
|
||||
new_transit_loc = "Solar Orbit"
|
||||
|
||||
elif is_arrival:
|
||||
new_st = "transit-complete"
|
||||
|
||||
# Also check existing state_label
|
||||
if nst and nst.lower() in ("transfer", "in transit"):
|
||||
if not new_st:
|
||||
# Fallback: try to infer from title
|
||||
if any(w in ctx for w in ["departure", "depart", "leave"]):
|
||||
new_st = "start-transit"
|
||||
new_transit_loc = "Solar Orbit"
|
||||
elif any(w in ctx for w in ["arrival", "arriv", "arrive"]):
|
||||
new_st = "transit-complete"
|
||||
else:
|
||||
# Keep as Transfer if we can't determine
|
||||
new_st = "start-transit"
|
||||
new_transit_loc = "Solar Orbit"
|
||||
|
||||
if new_st:
|
||||
# Determine previous_location for start-transit
|
||||
if new_st == "start-transit":
|
||||
# Get previous location from:
|
||||
# a) Entry-level context (where is the asset before this mission?)
|
||||
# b) Explicit departure body
|
||||
body_match = re.search(
|
||||
r'\b(earth|mars|europa|jupiter|saturn|venus|mercury|neptune|uranus|vesta|ceres|titan|io|ganymede|callisto|triton|pluto|leo|geo)\b',
|
||||
ctx
|
||||
)
|
||||
if body_match:
|
||||
body = body_match.group(1)
|
||||
body_map = {
|
||||
"earth": "LEO", "leo": "LEO", "geo": "GEO",
|
||||
"mars": "Mars Orbit", "venus": "Venus Orbit",
|
||||
"mercury": "Mercury Orbit", "jupiter": "Jupiter System",
|
||||
"europa": "Europa Orbit", "io": "Io Orbit",
|
||||
"ganymede": "Ganymede Orbit", "callisto": "Callisto Orbit",
|
||||
"saturn": "Saturn System", "titan": "Titan Orbit",
|
||||
"uranus": "Uranus System", "neptune": "Neptune System",
|
||||
"triton": "Triton Orbit", "vesta": "Vesta Orbit",
|
||||
"ceres": "Ceres Orbit", "pluto": "Pluto Orbit",
|
||||
}
|
||||
new_prev_loc = body_map.get(body, "LEO")
|
||||
else:
|
||||
new_prev_loc = nloc or "LEO"
|
||||
|
||||
# transit_location
|
||||
if not new_transit_loc:
|
||||
new_transit_loc = "Solar Orbit"
|
||||
|
||||
# For transit-complete, the location IS the arrival location
|
||||
if new_st == "transit-complete" and not nloc:
|
||||
# Try to infer arrival location from title
|
||||
body_match = re.search(
|
||||
r'\b(earth|mars|europa|jupiter|saturn|venus|mercury|neptune|uranus|vesta|titan|io|ganymede|callisto|triton)\b',
|
||||
ctx
|
||||
)
|
||||
if body_match:
|
||||
body = body_match.group(1)
|
||||
body_map = {
|
||||
"earth": "LEO", "mars": "Mars Orbit", "venus": "Venus Orbit",
|
||||
"mercury": "Mercury Orbit", "jupiter": "Jupiter System",
|
||||
"europa": "Europa Orbit", "io": "Io Orbit",
|
||||
"ganymede": "Ganymede Orbit", "callisto": "Callisto Orbit",
|
||||
"saturn": "Saturn System", "titan": "Titan Orbit",
|
||||
"uranus": "Uranus System", "neptune": "Neptune System",
|
||||
"triton": "Triton Orbit", "vesta": "Vesta Orbit",
|
||||
}
|
||||
nloc = body_map.get(body, nloc)
|
||||
|
||||
cur.execute("""
|
||||
UPDATE asset_state_nodes SET
|
||||
state_label = %s,
|
||||
previous_location = COALESCE(%s, previous_location),
|
||||
transit_location = COALESCE(%s, transit_location),
|
||||
location = COALESCE(%s, location),
|
||||
updated_at = %s
|
||||
WHERE id = %s
|
||||
""", (new_st, new_prev_loc, new_transit_loc, nloc, NOW, nid))
|
||||
updated += 1
|
||||
|
||||
conn.commit()
|
||||
print(f"Updated {updated} state nodes with new state labels")
|
||||
|
||||
# Stats
|
||||
cur.execute("SELECT state_label, COUNT(*) FROM asset_state_nodes GROUP BY state_label ORDER BY COUNT(*) DESC")
|
||||
print("\nState label distribution:")
|
||||
for r in cur.fetchall():
|
||||
print(f" {r[0] or 'NULL':25s}: {r[1]}")
|
||||
|
||||
cur.execute("SELECT COUNT(*), COUNT(CASE WHEN previous_location IS NOT NULL THEN 1 END), COUNT(CASE WHEN transit_location IS NOT NULL THEN 1 END) FROM asset_state_nodes")
|
||||
r = cur.fetchone()
|
||||
print(f"\nTotal: {r[0]}, with prev_loc: {r[1]}, with transit_loc: {r[2]}")
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user