Skip to content

Commit 1587b3a

Browse files
planet.jeroenclaude
andcommitted
fix(viewer): satellite table UX — JS syntax, entity selection, button positioning
- Fix JS syntax error in renderSatTable: \' in Python f-string produced '' (empty string) instead of \' (escaped quote), killing the entire <script> block — root cause of black page with no globe - Fix flyToSat entity ID: search correct dataSource, try both satellite-N (animated) and snapshot-N (snapshot mode) CZML ID formats - Fix table row click: use _sat_idx from row data instead of forEach index so sorting doesn't break entity lookup - Move table toggle button from bottom:8px to bottom:36px (clear timeline) - Hide table button when table is open; add close button in table header - Set viewer.selectedEntity on row click to show Cesium InfoBox with orbital parameters (altitude, inclination, velocity, beta angle, etc.) - Add _sat_idx field to get_satellite_table API response - 8 new tests (307 total), 0 regressions Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 96492ff commit 1587b3a

File tree

6 files changed

+164
-16
lines changed

6 files changed

+164
-16
lines changed

packages/core/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "humeris-core"
7-
version = "1.27.0"
7+
version = "1.28.1"
88
description = "Astrodynamics library for satellite constellation design — MIT open core"
99
requires-python = ">=3.11"
1010
license = "MIT"

packages/pro/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "humeris-pro"
7-
version = "1.27.0"
7+
version = "1.28.1"
88
description = "Humeris professional modules — advanced astrodynamics analysis"
99
requires-python = ">=3.11"
1010
license = {text = "Commercial — see COMMERCIAL-LICENSE.md"}

packages/pro/src/humeris/adapters/cesium_viewer.py

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ def generate_interactive_html(
558558
#satTable .sort-asc::after {{ content: " \\25B2"; font-size: 8px; }}
559559
#satTable .sort-desc::after {{ content: " \\25BC"; font-size: 8px; }}
560560
#satTableToggle {{
561-
position: fixed; bottom: 8px; right: 8px; z-index: 101;
561+
position: fixed; bottom: 36px; right: 8px; z-index: 101;
562562
background: rgba(30,40,60,0.9); color: #fff;
563563
border: 1px solid rgba(80,160,255,0.3); border-radius: 4px;
564564
padding: 4px 10px; cursor: pointer; font-size: 11px;
@@ -907,8 +907,11 @@ def generate_interactive_html(
907907
908908
function toggleSatTable() {{
909909
var tbl = document.getElementById("satTable");
910+
var btn = document.getElementById("satTableToggle");
910911
tbl.classList.toggle("visible");
911-
if (tbl.classList.contains("visible") && !satTableData) {{
912+
var isVisible = tbl.classList.contains("visible");
913+
btn.style.display = isVisible ? "none" : "";
914+
if (isVisible && !satTableData) {{
912915
// Load table for first constellation layer
913916
apiGet("/api/state").then(function(state) {{
914917
var constLayer = state.layers.find(function(l) {{ return l.category === "Constellation"; }});
@@ -941,15 +944,19 @@ def generate_interactive_html(
941944
return asc ? va - vb : vb - va;
942945
}});
943946
}}
944-
var html = '<table><thead><tr>';
947+
var html = '<div style="display:flex;justify-content:space-between;align-items:center;padding:4px 8px;background:rgba(30,35,50,0.98);border-bottom:1px solid rgba(255,255,255,0.15)">';
948+
html += '<span style="font-weight:bold;font-size:11px;color:rgba(255,255,255,0.7)">Satellite Table (' + rows.length + ')</span>';
949+
html += '<button onclick="toggleSatTable()" style="background:none;border:none;color:rgba(255,255,255,0.6);cursor:pointer;font-size:16px;padding:0 4px" title="Close table">&times;</button>';
950+
html += '</div>';
951+
html += '<table><thead><tr>';
945952
cols.forEach(function(c) {{
946953
var cls = "";
947954
if (satTableSort.col === c) cls = satTableSort.asc ? "sort-asc" : "sort-desc";
948-
html += '<th class="' + cls + '" onclick="sortSatTable(\'' + escapeHtml(c) + '\')">' + escapeHtml(c.replace(/_/g, " ")) + '</th>';
955+
html += '<th class="' + cls + '" onclick="sortSatTable(\\'' + escapeHtml(c) + '\\')">' + escapeHtml(c.replace(/_/g, " ")) + '</th>';
949956
}});
950957
html += '</tr></thead><tbody>';
951-
rows.forEach(function(row, idx) {{
952-
html += '<tr onclick="flyToSat(' + idx + ')">';
958+
rows.forEach(function(row) {{
959+
html += '<tr onclick="flyToSat(' + row._sat_idx + ')">';
953960
cols.forEach(function(c) {{
954961
html += '<td>' + escapeHtml(row[c]) + '</td>';
955962
}});
@@ -971,14 +978,14 @@ def generate_interactive_html(
971978
972979
function flyToSat(idx) {{
973980
if (!satTableLayerId) return;
974-
var entityId = satTableLayerId + "-" + idx;
975-
var ds = viewer.dataSources;
976-
for (var i = 0; i < ds.length; i++) {{
977-
var entity = ds.get(i).entities.getById(entityId);
978-
if (entity) {{
979-
viewer.flyTo(entity, {{duration: 1.5}});
980-
return;
981-
}}
981+
var ds = layerSources[satTableLayerId];
982+
if (!ds) return;
983+
// CZML uses satellite-N (animated) or snapshot-N (snapshot mode)
984+
var entity = ds.entities.getById("satellite-" + idx)
985+
|| ds.entities.getById("snapshot-" + idx);
986+
if (entity) {{
987+
viewer.selectedEntity = entity;
988+
viewer.flyTo(entity, {{duration: 1.5}});
982989
}}
983990
}}
984991

packages/pro/src/humeris/adapters/viewer_server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,7 @@ def get_satellite_table(self, layer_id: str) -> dict[str, Any]:
12421242
except Exception:
12431243
eclipse_pct = 0.0
12441244
rows.append({
1245+
"_sat_idx": idx,
12451246
"name": name,
12461247
"plane": plane,
12471248
"altitude_km": round(alt_km, 2),

tests/test_cesium_viewer.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,128 @@ def test_remove_data_source_with_destroy(self):
979979
"dataSources.remove should pass true for destroy"
980980

981981

982+
class TestViewerTableUX:
983+
"""VIEWER-TABLE-UX: table button position and satellite detail selection."""
984+
985+
def _html(self):
986+
from humeris.adapters.cesium_viewer import generate_interactive_html
987+
return generate_interactive_html()
988+
989+
# UXC1: Table toggle button must clear the Cesium timeline
990+
def test_table_toggle_button_clears_timeline(self):
991+
"""#satTableToggle bottom >= 36px to avoid overlapping the timeline."""
992+
html = self._html()
993+
match = re.search(r'#satTableToggle\s*\{([^}]+)\}', html)
994+
assert match is not None, "satTableToggle CSS rule not found"
995+
css = match.group(1)
996+
bottom_match = re.search(r'bottom:\s*(\d+)px', css)
997+
assert bottom_match is not None, "bottom property not found in satTableToggle"
998+
bottom_px = int(bottom_match.group(1))
999+
assert bottom_px >= 36, (
1000+
f"satTableToggle bottom is {bottom_px}px, must be >= 36px "
1001+
"to clear the Cesium timeline widget"
1002+
)
1003+
1004+
# UXC2: flyToSat must set viewer.selectedEntity for InfoBox display
1005+
def test_fly_to_sat_sets_selected_entity(self):
1006+
"""flyToSat must set viewer.selectedEntity so Cesium InfoBox shows details."""
1007+
html = self._html()
1008+
# Find the flyToSat function body
1009+
match = re.search(
1010+
r'function flyToSat\([^)]*\)\s*\{(.*?)\n\s{8}\}',
1011+
html, re.DOTALL,
1012+
)
1013+
assert match is not None, "flyToSat function not found"
1014+
body = match.group(1)
1015+
assert "viewer.selectedEntity" in body, (
1016+
"flyToSat must set viewer.selectedEntity to activate "
1017+
"Cesium's InfoBox with satellite details"
1018+
)
1019+
1020+
# UXC3: flyToSat must use CZML entity ID formats (satellite-N and snapshot-N)
1021+
def test_fly_to_sat_uses_czml_entity_ids(self):
1022+
"""flyToSat must try both satellite-{idx} and snapshot-{idx} CZML ID formats."""
1023+
html = self._html()
1024+
match = re.search(
1025+
r'function flyToSat\([^)]*\)\s*\{(.*?)\n\s{8}\}',
1026+
html, re.DOTALL,
1027+
)
1028+
assert match is not None, "flyToSat function not found"
1029+
body = match.group(1)
1030+
assert '"satellite-"' in body, (
1031+
"flyToSat must try 'satellite-' prefix for animated mode CZML entities"
1032+
)
1033+
assert '"snapshot-"' in body, (
1034+
"flyToSat must try 'snapshot-' prefix for snapshot mode CZML entities"
1035+
)
1036+
1037+
# UXC4: Table rows pass original sat index, not sorted position
1038+
def test_table_rows_use_sat_idx_not_position(self):
1039+
"""Table row onclick must use row._sat_idx, not the forEach index."""
1040+
html = self._html()
1041+
match = re.search(
1042+
r'function renderSatTable\(\)\s*\{(.*?)\n\s{8}\}',
1043+
html, re.DOTALL,
1044+
)
1045+
assert match is not None, "renderSatTable function not found"
1046+
body = match.group(1)
1047+
assert "_sat_idx" in body, (
1048+
"renderSatTable must use row._sat_idx for flyToSat, "
1049+
"not the forEach index which changes with sorting"
1050+
)
1051+
1052+
# UXC5: Table toggle button hides when table is visible
1053+
def test_table_toggle_hides_when_table_open(self):
1054+
"""toggleSatTable must hide the button when table becomes visible."""
1055+
html = self._html()
1056+
match = re.search(
1057+
r'function toggleSatTable\(\)\s*\{(.*?)\n\s{8}\}',
1058+
html, re.DOTALL,
1059+
)
1060+
assert match is not None, "toggleSatTable function not found"
1061+
body = match.group(1)
1062+
assert "satTableToggle" in body, (
1063+
"toggleSatTable must reference the toggle button to hide/show it"
1064+
)
1065+
assert 'display' in body or 'none' in body or 'style' in body, (
1066+
"toggleSatTable must change button display when table state changes"
1067+
)
1068+
1069+
# UXC6: Table has a close button
1070+
def test_table_has_close_button(self):
1071+
"""The satellite table must include a close button in its rendered HTML."""
1072+
html = self._html()
1073+
match = re.search(
1074+
r'function renderSatTable\(\)\s*\{(.*?)\n\s{8}\}',
1075+
html, re.DOTALL,
1076+
)
1077+
assert match is not None, "renderSatTable function not found"
1078+
body = match.group(1)
1079+
assert "toggleSatTable" in body or "closeSatTable" in body, (
1080+
"renderSatTable must include a close button that hides the table"
1081+
)
1082+
1083+
# UXC7: Generated JS must have no syntax errors
1084+
def test_generated_js_syntax_valid(self):
1085+
"""Generated JavaScript must pass syntax validation."""
1086+
import subprocess
1087+
html = self._html()
1088+
match = re.search(r'<script>(.*?)</script>', html, re.DOTALL)
1089+
assert match is not None, "No script block found"
1090+
js = match.group(1)
1091+
result = subprocess.run(
1092+
["node", "--check"],
1093+
input=js,
1094+
capture_output=True,
1095+
text=True,
1096+
timeout=10,
1097+
)
1098+
assert result.returncode == 0, (
1099+
f"JavaScript syntax error in generated viewer HTML:\n"
1100+
f"{result.stderr.strip()}"
1101+
)
1102+
1103+
9821104
class TestCesiumViewerPurity:
9831105
"""Adapter purity: only stdlib + internal imports allowed."""
9841106

tests/test_viewer_server.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2964,6 +2964,24 @@ def test_table_plane_assignment(self):
29642964
# First 2 sats in plane 0, next 2 in plane 1
29652965
assert planes == [0, 0, 1, 1]
29662966

2967+
def test_table_rows_include_sat_idx(self):
2968+
"""Each row must include _sat_idx for stable entity lookup after sorting."""
2969+
from humeris.adapters.viewer_server import LayerManager
2970+
mgr = LayerManager(epoch=EPOCH)
2971+
states = _make_states(n_planes=2, n_sats=3)
2972+
lid = mgr.add_layer(
2973+
name="Constellation:Walker",
2974+
category="Constellation",
2975+
layer_type="walker",
2976+
states=states,
2977+
params={"altitude_km": 550, "inclination_deg": 53, "num_planes": 2, "sats_per_plane": 3},
2978+
sat_names=[f"Sat-{i}" for i in range(6)],
2979+
)
2980+
table = mgr.get_satellite_table(lid)
2981+
for i, row in enumerate(table["rows"]):
2982+
assert "_sat_idx" in row, f"Row {i} missing _sat_idx field"
2983+
assert row["_sat_idx"] == i, f"Row {i} _sat_idx={row['_sat_idx']}, expected {i}"
2984+
29672985

29682986
# ---------------------------------------------------------------------------
29692987
# APP-04: Named scenarios with description

0 commit comments

Comments
 (0)