From b77c94a6d8e7c99318c5adb9d0253af1e6516d92 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 24 Mar 2026 18:19:29 +0100 Subject: [PATCH] feat(atlas): add sync-wiki command, sync Gateway body catalog to wiki MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New atlas subcommand: sync-wiki generates a Celestial Bodies section in wiki pages from DB body/station data. All fields rendered: orbit, ID, name, type, inhabited, population, mass, gravity, atmosphere, biome, hydrosphere, economy, settlement pattern, industrial corridor. Stations table includes docking class, governance, gate flag, districts. Moons render indented under their parent body (↳ notation). Section is idempotent — re-running overwrites the existing section. DB is source of truth, wiki is generated output. No import direction. Co-Authored-By: Claude Opus 4.6 (1M context) --- server/src/bin/atlas.rs | 307 +++++++++++++++++++++++++++++++ wiki/star-systems/GJ-71/index.md | 20 ++ 2 files changed, 327 insertions(+) diff --git a/server/src/bin/atlas.rs b/server/src/bin/atlas.rs index ba4f754f9..e8c5accdc 100644 --- a/server/src/bin/atlas.rs +++ b/server/src/bin/atlas.rs @@ -147,6 +147,14 @@ enum Commands { }, /// List systems that have no bodies yet Unfinished, + /// Sync body/station data into wiki page for a system (or all systems) + SyncWiki { + /// System ID (omit for all systems with bodies) + system_id: Option, + /// Path to wiki directory (default: wiki/star-systems/) + #[arg(long, default_value = "wiki/star-systems")] + wiki: String, + }, } // --------------------------------------------------------------------------- @@ -1233,6 +1241,302 @@ fn cmd_unfinished(conn: &Connection) { println!("{}", serde_json::to_string_pretty(&result).unwrap()); } +fn cmd_sync_wiki(conn: &Connection, system_id: Option<&str>, wiki_dir: &str) { + // Get list of systems to sync + let system_ids: Vec = if let Some(sid) = system_id { + vec![sid.to_string()] + } else { + let mut stmt = conn + .prepare("SELECT DISTINCT system_id FROM bodies ORDER BY system_id") + .unwrap(); + stmt.query_map([], |row| row.get::<_, String>(0)) + .unwrap() + .filter_map(|r| r.ok()) + .collect() + }; + + let mut synced = 0; + + for sid in &system_ids { + let sid_slug = sid.replace(' ', "-"); + let wiki_path = std::path::Path::new(wiki_dir).join(&sid_slug).join("index.md"); + if !wiki_path.exists() { + eprintln!("skip: {} — no wiki page at {}", sid, wiki_path.display()); + continue; + } + + // Query bodies + let mut body_stmt = conn + .prepare( + "SELECT body_id, proper_name, body_type, orbit_index, parent_body_id, + inhabited, population, mass_class, atmosphere, surface_gravity, + biome_summary, hydrosphere, economic_role, settlement_pattern, + industrial_corridor + FROM bodies WHERE system_id = ?1 + ORDER BY CASE WHEN parent_body_id IS NULL THEN orbit_index ELSE 1000 + orbit_index END", + ) + .unwrap(); + + struct Body { + id: String, + name: Option, + btype: String, + orbit: i32, + parent: Option, + inhabited: bool, + population: i64, + mass_class: Option, + atmosphere: Option, + gravity: Option, + biome: Option, + hydro: Option, + econ: Option, + settlement: Option, + industrial: Option, + } + + let bodies: Vec = body_stmt + .query_map(params![sid], |row| { + Ok(Body { + id: row.get(0)?, + name: row.get(1)?, + btype: row.get(2)?, + orbit: row.get::<_, Option>(3)?.unwrap_or(0), + parent: row.get(4)?, + inhabited: row.get::<_, i32>(5)? != 0, + population: row.get::<_, Option>(6)?.unwrap_or(0), + mass_class: row.get(7)?, + atmosphere: row.get(8)?, + gravity: row.get(9)?, + biome: row.get(10)?, + hydro: row.get(11)?, + econ: row.get(12)?, + settlement: row.get(13)?, + industrial: row.get(14)?, + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + // Query stations + let mut station_stmt = conn + .prepare( + "SELECT station_id, proper_name, orbits_body_id, station_type, + population, economic_role, governance_type, docking_class, + has_gate_infrastructure, district_count + FROM stations WHERE system_id = ?1 + ORDER BY station_id", + ) + .unwrap(); + + struct Station { + id: String, + name: Option, + orbits: Option, + stype: String, + population: i64, + econ: Option, + governance: Option, + docking: Option, + gate: bool, + districts: i32, + } + + let stations: Vec = station_stmt + .query_map(params![sid], |row| { + Ok(Station { + id: row.get(0)?, + name: row.get(1)?, + orbits: row.get(2)?, + stype: row.get(3)?, + population: row.get::<_, Option>(4)?.unwrap_or(0), + econ: row.get(5)?, + governance: row.get(6)?, + docking: row.get(7)?, + gate: row.get::<_, Option>(8)?.unwrap_or(0) != 0, + districts: row.get::<_, Option>(9)?.unwrap_or(1), + }) + }) + .unwrap() + .filter_map(|r| r.ok()) + .collect(); + + if bodies.is_empty() && stations.is_empty() { + continue; + } + + // Build the Celestial Bodies section + let mut section = String::new(); + section.push_str("## Celestial Bodies\n"); + section.push_str("\n\n"); + + // Bodies table + if !bodies.is_empty() { + section.push_str("| Orbit | ID | Name | Type | Inhabited | Population | Mass | Gravity | Atmosphere | Biome | Hydrosphere | Economy | Settlement | Industrial |\n"); + section.push_str("|---|---|---|---|---|---|---|---|---|---|---|---|---|---|\n"); + + // First pass: top-level bodies (no parent) + for b in &bodies { + if b.parent.is_some() { + continue; + } + let name = b.name.as_deref().unwrap_or("—"); + let pop = if b.population > 0 { + format_population(b.population) + } else { + "—".to_string() + }; + let grav = b.gravity.map(|g| format!("{:.2}g", g)).unwrap_or_else(|| "—".to_string()); + section.push_str(&format!( + "| {} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", + b.orbit, + b.id, + name, + b.btype, + if b.inhabited { "yes" } else { "no" }, + pop, + b.mass_class.as_deref().unwrap_or("—"), + grav, + b.atmosphere.as_deref().unwrap_or("—"), + b.biome.as_deref().unwrap_or("—"), + b.hydro.as_deref().unwrap_or("—"), + b.econ.as_deref().unwrap_or("—"), + b.settlement.as_deref().unwrap_or("—"), + b.industrial.as_deref().unwrap_or("—"), + )); + + // Child bodies (moons) + for m in &bodies { + if m.parent.as_deref() == Some(&b.id) { + let mname = m.name.as_deref().unwrap_or("—"); + let mpop = if m.population > 0 { + format_population(m.population) + } else { + "—".to_string() + }; + let mgrav = m.gravity.map(|g| format!("{:.2}g", g)).unwrap_or_else(|| "—".to_string()); + section.push_str(&format!( + "| ↳ {}.{} | `{}` | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} |\n", + b.orbit, + m.orbit, + m.id, + mname, + m.btype, + if m.inhabited { "yes" } else { "no" }, + mpop, + m.mass_class.as_deref().unwrap_or("—"), + mgrav, + m.atmosphere.as_deref().unwrap_or("—"), + m.biome.as_deref().unwrap_or("—"), + m.hydro.as_deref().unwrap_or("—"), + m.econ.as_deref().unwrap_or("—"), + m.settlement.as_deref().unwrap_or("—"), + m.industrial.as_deref().unwrap_or("—"), + )); + } + } + } + section.push('\n'); + } + + // Stations table + if !stations.is_empty() { + section.push_str("### Stations & Facilities\n\n"); + section.push_str("| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts |\n"); + section.push_str("|---|---|---|---|---|---|---|---|---|---|\n"); + for s in &stations { + let name = s.name.as_deref().unwrap_or("—"); + let orbits = s.orbits.as_deref().unwrap_or("—"); + let pop = if s.population > 0 { + format_population(s.population) + } else { + "—".to_string() + }; + section.push_str(&format!( + "| `{}` | {} | {} | `{}` | {} | {} | {} | {} | {} | {} |\n", + s.id, + name, + s.stype, + orbits, + pop, + s.econ.as_deref().unwrap_or("—"), + s.governance.as_deref().unwrap_or("—"), + s.docking.as_deref().unwrap_or("—"), + if s.gate { "yes" } else { "no" }, + s.districts, + )); + } + section.push('\n'); + } + + // Read current wiki content + let content = std::fs::read_to_string(&wiki_path).unwrap(); + + // Replace or insert the Celestial Bodies section + let marker_start = "## Celestial Bodies"; + let new_content = if let Some(start_pos) = content.find(marker_start) { + // Find the next ## heading after the section (or end of file) + let after = &content[start_pos + marker_start.len()..]; + let end_offset = after + .find("\n## ") + .map(|p| start_pos + marker_start.len() + p + 1) + .unwrap_or(content.len()); + format!("{}{}\n{}", &content[..start_pos], section.trim_end(), &content[end_offset..]) + } else { + // Insert after the System Profile section (after the first "---" pair) + // Find the second "---" which ends the System Profile + let mut dashes = 0; + let mut insert_pos = None; + for (i, line) in content.lines().enumerate() { + if line.trim() == "---" { + dashes += 1; + if dashes == 3 { + // Third --- is end of System Profile + let byte_pos: usize = content.lines().take(i + 1).map(|l| l.len() + 1).sum(); + insert_pos = Some(byte_pos); + break; + } + } + } + if let Some(pos) = insert_pos { + format!("{}\n{}\n---\n\n{}", &content[..pos], section.trim_end(), &content[pos..]) + } else { + // Fallback: append at end + format!("{}\n{}", content, section) + } + }; + + std::fs::write(&wiki_path, new_content).unwrap(); + eprintln!("synced: {} → {}", sid, wiki_path.display()); + synced += 1; + } + + #[derive(Serialize)] + struct SyncResult { + systems_synced: usize, + } + println!( + "{}", + serde_json::to_string_pretty(&SyncResult { + systems_synced: synced + }) + .unwrap() + ); +} + +fn format_population(pop: i64) -> String { + if pop >= 1_000_000_000 { + format!("{:.1}B", pop as f64 / 1_000_000_000.0) + } else if pop >= 1_000_000 { + format!("{}M", pop / 1_000_000) + } else if pop >= 1_000 { + format!("{}K", pop / 1_000) + } else { + format!("{}", pop) + } +} + // cmd_populate removed — replaced by per-system author/commit workflow // --------------------------------------------------------------------------- @@ -1261,5 +1565,8 @@ fn main() { Commands::CommitSystem { path } => cmd_commit_system(&conn, path), Commands::WipeSystem { system_id } => cmd_wipe_system(&conn, system_id), Commands::Unfinished => cmd_unfinished(&conn), + Commands::SyncWiki { system_id, wiki } => { + cmd_sync_wiki(&conn, system_id.as_deref(), wiki) + } } } diff --git a/wiki/star-systems/GJ-71/index.md b/wiki/star-systems/GJ-71/index.md index 58c060ae5..b9b7d8e93 100644 --- a/wiki/star-systems/GJ-71/index.md +++ b/wiki/star-systems/GJ-71/index.md @@ -66,3 +66,23 @@ The Institute has been studying the first horizon station for five hundred years - **Hop Distance from Gateway:** 0 - **Adjacent Systems:** GJ 0, GJ 559B, GJ 144, GJ 244A, GJ 725B + +## Celestial Bodies + + +| Orbit | ID | Name | Type | Inhabited | Population | Mass | Gravity | Atmosphere | Biome | Hydrosphere | Economy | Settlement | Industrial | +|---|---|---|---|---|---|---|---|---|---|---|---|---|---| +| 1 | `GJ71b` | — | planet | no | — | terrestrial | — | none | barren | none | — | — | — | +| 2 | `GJ71c` | Threshold | planet | yes | 600M | terrestrial | — | breathable | temperate | ocean | agricultural | urban_concentrated | — | +| 3 | `GJ71d` | Arden | planet | yes | 500M | terrestrial | — | breathable | temperate | rivers | agricultural | dispersed_rural | — | +| ↳ 3.1 | `GJ71d-1` | Verantis | moon | yes | 20M | dwarf | — | none | barren | none | transit | domed | — | +| 4 | `GJ71e` | — | planet | no | — | terrestrial | — | thin | frozen | ice | — | — | — | +| 5 | `GJ71-belt` | — | asteroid_belt | no | — | — | — | — | — | — | — | — | — | +| 6 | `GJ71-oort` | — | oort_cloud | no | — | — | — | — | — | — | — | — | — | + +### Stations & Facilities + +| ID | Name | Type | Orbits | Population | Economy | Governance | Docking | Gate | Districts | +|---|---|---|---|---|---|---|---|---|---| +| `GJ71-oort-S1` | Horizon Station | horizon | `GJ71-oort` | 80M | transit | — | major | yes | 1 | +