""" 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.")