gea-program 0.74.0__tar.gz → 0.75.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (395) hide show
  1. {gea_program-0.74.0/gea_program.egg-info → gea_program-0.75.1}/PKG-INFO +27 -4
  2. {gea_program-0.74.0 → gea_program-0.75.1}/README.md +26 -3
  3. {gea_program-0.74.0 → gea_program-0.75.1}/gea/__init__.py +1 -1
  4. {gea_program-0.74.0 → gea_program-0.75.1}/gea/__main__.py +47 -0
  5. {gea_program-0.74.0 → gea_program-0.75.1}/gea/acceptance_tests.py +221 -1
  6. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/IT_SECURITY.md +19 -0
  7. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/install.md +2 -2
  8. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/reader.md +1 -1
  9. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/recorder.md +2 -2
  10. gea_program-0.75.1/gea/help/vpn.md +13 -0
  11. {gea_program-0.74.0 → gea_program-0.75.1}/gea/helplib.py +1 -0
  12. {gea_program-0.74.0 → gea_program-0.75.1}/gea/service.py +52 -0
  13. gea_program-0.75.1/gea/vpn.py +407 -0
  14. {gea_program-0.74.0 → gea_program-0.75.1}/gea/web/app.html +28 -2
  15. gea_program-0.75.1/gea/x25519.py +99 -0
  16. {gea_program-0.74.0 → gea_program-0.75.1/gea_program.egg-info}/PKG-INFO +27 -4
  17. {gea_program-0.74.0 → gea_program-0.75.1}/gea_program.egg-info/SOURCES.txt +3 -0
  18. {gea_program-0.74.0 → gea_program-0.75.1}/pyproject.toml +1 -1
  19. {gea_program-0.74.0 → gea_program-0.75.1}/LICENSE +0 -0
  20. {gea_program-0.74.0 → gea_program-0.75.1}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  21. {gea_program-0.74.0 → gea_program-0.75.1}/gea/accuracy_statement.py +0 -0
  22. {gea_program-0.74.0 → gea_program-0.75.1}/gea/alarm_engine.py +0 -0
  23. {gea_program-0.74.0 → gea_program-0.75.1}/gea/alerts.py +0 -0
  24. {gea_program-0.74.0 → gea_program-0.75.1}/gea/backup.py +0 -0
  25. {gea_program-0.74.0 → gea_program-0.75.1}/gea/bench.py +0 -0
  26. {gea_program-0.74.0 → gea_program-0.75.1}/gea/bit_seismic.py +0 -0
  27. {gea_program-0.74.0 → gea_program-0.75.1}/gea/blind_harness.py +0 -0
  28. {gea_program-0.74.0 → gea_program-0.75.1}/gea/casing_design.py +0 -0
  29. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  30. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  31. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  32. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  33. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  34. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/barbados_110_consolidation.txt +0 -0
  35. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  36. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  37. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  38. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  39. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  40. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  41. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  42. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  43. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  44. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  45. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  46. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  47. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  48. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  49. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  50. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  51. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  52. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  53. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  54. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  55. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  56. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  57. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  58. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  59. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  60. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  61. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  62. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  63. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  64. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  65. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  66. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  67. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  68. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  69. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  70. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  71. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  72. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  73. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  74. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  75. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  76. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  77. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  78. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  79. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  80. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  81. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  82. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  83. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  84. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  85. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  86. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  87. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  88. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  89. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/l06_06_nl_survey.csv +0 -0
  90. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  91. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  92. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  93. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  94. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  95. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  96. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/med_160_sapropels.txt +0 -0
  97. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  98. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  99. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  100. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  101. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  102. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  103. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  104. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  105. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  106. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  107. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  108. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  109. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  110. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  111. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  112. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  113. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  114. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  115. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  116. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  117. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  118. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  119. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  120. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  121. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  122. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  123. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  124. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  125. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  126. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  127. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  128. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  129. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  130. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  131. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  132. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  133. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog_map.py +0 -0
  134. {gea_program-0.74.0 → gea_program-0.75.1}/gea/catalog_register.py +0 -0
  135. {gea_program-0.74.0 → gea_program-0.75.1}/gea/certificates.py +0 -0
  136. {gea_program-0.74.0 → gea_program-0.75.1}/gea/cli.py +0 -0
  137. {gea_program-0.74.0 → gea_program-0.75.1}/gea/client_reports.py +0 -0
  138. {gea_program-0.74.0 → gea_program-0.75.1}/gea/collision.py +0 -0
  139. {gea_program-0.74.0 → gea_program-0.75.1}/gea/config_versioning.py +0 -0
  140. {gea_program-0.74.0 → gea_program-0.75.1}/gea/conformance.py +0 -0
  141. {gea_program-0.74.0 → gea_program-0.75.1}/gea/control_room.py +0 -0
  142. {gea_program-0.74.0 → gea_program-0.75.1}/gea/correlation.py +0 -0
  143. {gea_program-0.74.0 → gea_program-0.75.1}/gea/dashboard.py +0 -0
  144. {gea_program-0.74.0 → gea_program-0.75.1}/gea/declarations.py +0 -0
  145. {gea_program-0.74.0 → gea_program-0.75.1}/gea/decline.py +0 -0
  146. {gea_program-0.74.0 → gea_program-0.75.1}/gea/deviation.py +0 -0
  147. {gea_program-0.74.0 → gea_program-0.75.1}/gea/dlis.py +0 -0
  148. {gea_program-0.74.0 → gea_program-0.75.1}/gea/dnp3.py +0 -0
  149. {gea_program-0.74.0 → gea_program-0.75.1}/gea/doctor.py +0 -0
  150. {gea_program-0.74.0 → gea_program-0.75.1}/gea/dossier.py +0 -0
  151. {gea_program-0.74.0 → gea_program-0.75.1}/gea/dossier_verify.py +0 -0
  152. {gea_program-0.74.0 → gea_program-0.75.1}/gea/downhole_engine.py +0 -0
  153. {gea_program-0.74.0 → gea_program-0.75.1}/gea/drift_monitor.py +0 -0
  154. {gea_program-0.74.0 → gea_program-0.75.1}/gea/drill_string.py +0 -0
  155. {gea_program-0.74.0 → gea_program-0.75.1}/gea/earth_model.py +0 -0
  156. {gea_program-0.74.0 → gea_program-0.75.1}/gea/economics.py +0 -0
  157. {gea_program-0.74.0 → gea_program-0.75.1}/gea/ed25519.py +0 -0
  158. {gea_program-0.74.0 → gea_program-0.75.1}/gea/edr.py +0 -0
  159. {gea_program-0.74.0 → gea_program-0.75.1}/gea/elevation.py +0 -0
  160. {gea_program-0.74.0 → gea_program-0.75.1}/gea/etp.py +0 -0
  161. {gea_program-0.74.0 → gea_program-0.75.1}/gea/etp12_schemas.json +0 -0
  162. {gea_program-0.74.0 → gea_program-0.75.1}/gea/example_register_map.json +0 -0
  163. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fat_sat.py +0 -0
  164. {gea_program-0.74.0 → gea_program-0.75.1}/gea/field_pull.py +0 -0
  165. {gea_program-0.74.0 → gea_program-0.75.1}/gea/files.py +0 -0
  166. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fixtures/README.md +0 -0
  167. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  168. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  169. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  170. {gea_program-0.74.0 → gea_program-0.75.1}/gea/fixtures/ne_110m_coastline.json +0 -0
  171. {gea_program-0.74.0 → gea_program-0.75.1}/gea/follower.py +0 -0
  172. {gea_program-0.74.0 → gea_program-0.75.1}/gea/forward_model.py +0 -0
  173. {gea_program-0.74.0 → gea_program-0.75.1}/gea/frac_hits.py +0 -0
  174. {gea_program-0.74.0 → gea_program-0.75.1}/gea/friction_priors.py +0 -0
  175. {gea_program-0.74.0 → gea_program-0.75.1}/gea/gamma.py +0 -0
  176. {gea_program-0.74.0 → gea_program-0.75.1}/gea/gauge_aging.py +0 -0
  177. {gea_program-0.74.0 → gea_program-0.75.1}/gea/gauge_specs.py +0 -0
  178. {gea_program-0.74.0 → gea_program-0.75.1}/gea/geodesy.py +0 -0
  179. {gea_program-0.74.0 → gea_program-0.75.1}/gea/geomag.py +0 -0
  180. {gea_program-0.74.0 → gea_program-0.75.1}/gea/gravity_reference.py +0 -0
  181. {gea_program-0.74.0 → gea_program-0.75.1}/gea/hartip.py +0 -0
  182. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/TESTER_GUIDE.md +0 -0
  183. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/alarms.md +0 -0
  184. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/alerts.md +0 -0
  185. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/audit-update.md +0 -0
  186. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/backup.md +0 -0
  187. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/bit-image.md +0 -0
  188. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/casing.md +0 -0
  189. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/catalog.md +0 -0
  190. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/charts.md +0 -0
  191. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/collision.md +0 -0
  192. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/company-sign-in.md +0 -0
  193. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/conformance.md +0 -0
  194. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/control-room.md +0 -0
  195. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/data-in.md +0 -0
  196. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/declare.md +0 -0
  197. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/decline.md +0 -0
  198. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/dlis.md +0 -0
  199. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/doctor.md +0 -0
  200. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/dossier.md +0 -0
  201. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/drift.md +0 -0
  202. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/drillstring.md +0 -0
  203. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/economics.md +0 -0
  204. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/edr.md +0 -0
  205. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/etp.md +0 -0
  206. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/field-ports.md +0 -0
  207. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/files.md +0 -0
  208. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/frac-hits.md +0 -0
  209. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/historian.md +0 -0
  210. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/https.md +0 -0
  211. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/hydraulics.md +0 -0
  212. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/instruments.md +0 -0
  213. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/interval.md +0 -0
  214. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/invoice-check.md +0 -0
  215. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/map.md +0 -0
  216. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/month-end.md +0 -0
  217. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/notifications.md +0 -0
  218. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/opseis.md +0 -0
  219. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/osdu.md +0 -0
  220. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/pad-summary.md +0 -0
  221. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/patches.md +0 -0
  222. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/penetrator.md +0 -0
  223. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/plan.md +0 -0
  224. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/pore-pressure.md +0 -0
  225. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/ppdm.md +0 -0
  226. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/quakeml.md +0 -0
  227. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/quality.md +0 -0
  228. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/records.md +0 -0
  229. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/report-history.md +0 -0
  230. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/rig-time.md +0 -0
  231. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/rocks.md +0 -0
  232. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/seedlink.md +0 -0
  233. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/seismic.md +0 -0
  234. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/setup.md +0 -0
  235. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/simulation.md +0 -0
  236. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/site.md +0 -0
  237. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/sites.md +0 -0
  238. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/slip.md +0 -0
  239. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/soak.md +0 -0
  240. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/sra.md +0 -0
  241. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/start.md +0 -0
  242. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/structure.md +0 -0
  243. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/tools.md +0 -0
  244. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/torque-drag.md +0 -0
  245. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/transients.md +0 -0
  246. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/upkeep.md +0 -0
  247. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/vibration.md +0 -0
  248. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/well-report.md +0 -0
  249. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/well-tests.md +0 -0
  250. {gea_program-0.74.0 → gea_program-0.75.1}/gea/help/well3d.md +0 -0
  251. {gea_program-0.74.0 → gea_program-0.75.1}/gea/housekeeping.py +0 -0
  252. {gea_program-0.74.0 → gea_program-0.75.1}/gea/hydraulics.py +0 -0
  253. {gea_program-0.74.0 → gea_program-0.75.1}/gea/icon.py +0 -0
  254. {gea_program-0.74.0 → gea_program-0.75.1}/gea/interval.py +0 -0
  255. {gea_program-0.74.0 → gea_program-0.75.1}/gea/inverse_engine.py +0 -0
  256. {gea_program-0.74.0 → gea_program-0.75.1}/gea/invoice_check.py +0 -0
  257. {gea_program-0.74.0 → gea_program-0.75.1}/gea/jobs.py +0 -0
  258. {gea_program-0.74.0 → gea_program-0.75.1}/gea/launcher.py +0 -0
  259. {gea_program-0.74.0 → gea_program-0.75.1}/gea/live_ports.py +0 -0
  260. {gea_program-0.74.0 → gea_program-0.75.1}/gea/loadtest.py +0 -0
  261. {gea_program-0.74.0 → gea_program-0.75.1}/gea/matplotlib_demo.py +0 -0
  262. {gea_program-0.74.0 → gea_program-0.75.1}/gea/modbus.py +0 -0
  263. {gea_program-0.74.0 → gea_program-0.75.1}/gea/modbus_rtu.py +0 -0
  264. {gea_program-0.74.0 → gea_program-0.75.1}/gea/model_card.py +0 -0
  265. {gea_program-0.74.0 → gea_program-0.75.1}/gea/month_end.py +0 -0
  266. {gea_program-0.74.0 → gea_program-0.75.1}/gea/mqtt_port.py +0 -0
  267. {gea_program-0.74.0 → gea_program-0.75.1}/gea/notify.py +0 -0
  268. {gea_program-0.74.0 → gea_program-0.75.1}/gea/oidc.py +0 -0
  269. {gea_program-0.74.0 → gea_program-0.75.1}/gea/oidc_mock.py +0 -0
  270. {gea_program-0.74.0 → gea_program-0.75.1}/gea/op_seismicity.py +0 -0
  271. {gea_program-0.74.0 → gea_program-0.75.1}/gea/opcua_port.py +0 -0
  272. {gea_program-0.74.0 → gea_program-0.75.1}/gea/operator_app.py +0 -0
  273. {gea_program-0.74.0 → gea_program-0.75.1}/gea/osdu.py +0 -0
  274. {gea_program-0.74.0 → gea_program-0.75.1}/gea/pad_summary.py +0 -0
  275. {gea_program-0.74.0 → gea_program-0.75.1}/gea/patches.py +0 -0
  276. {gea_program-0.74.0 → gea_program-0.75.1}/gea/pdf_report.py +0 -0
  277. {gea_program-0.74.0 → gea_program-0.75.1}/gea/penetrator.py +0 -0
  278. {gea_program-0.74.0 → gea_program-0.75.1}/gea/permits.py +0 -0
  279. {gea_program-0.74.0 → gea_program-0.75.1}/gea/piwebapi.py +0 -0
  280. {gea_program-0.74.0 → gea_program-0.75.1}/gea/pore_pressure.py +0 -0
  281. {gea_program-0.74.0 → gea_program-0.75.1}/gea/ports.py +0 -0
  282. {gea_program-0.74.0 → gea_program-0.75.1}/gea/ppdm.py +0 -0
  283. {gea_program-0.74.0 → gea_program-0.75.1}/gea/profile_catalog.py +0 -0
  284. {gea_program-0.74.0 → gea_program-0.75.1}/gea/project.py +0 -0
  285. {gea_program-0.74.0 → gea_program-0.75.1}/gea/qt6_downhole_app.py +0 -0
  286. {gea_program-0.74.0 → gea_program-0.75.1}/gea/quakeml.py +0 -0
  287. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reader.py +0 -0
  288. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reader_app.py +0 -0
  289. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reconciler.py +0 -0
  290. {gea_program-0.74.0 → gea_program-0.75.1}/gea/recorder_link.py +0 -0
  291. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  292. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  293. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/WMM2025.COF +0 -0
  294. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  295. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/eia_prices.provenance.json +0 -0
  296. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/eia_wti_daily.csv.gz +0 -0
  297. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/iscwsa_rev4_example.json +0 -0
  298. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/iscwsa_rev5_example.json +0 -0
  299. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/l0701_sonic_gr.las.gz +0 -0
  300. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/l0701_sonic_gr.provenance.json +0 -0
  301. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/libmseed_steim.provenance.json +0 -0
  302. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  303. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  304. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/libmseed_steim2_512.mseed +0 -0
  305. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/mahoning_example.json +0 -0
  306. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/mud_motor_specs.json +0 -0
  307. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/owsg_tool_models.json +0 -0
  308. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/penetrator_ratings.json +0 -0
  309. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/seismicity_protocols.json +0 -0
  310. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/tool_sources.json +0 -0
  311. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/us_outline.json +0 -0
  312. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/us_outline.provenance.json +0 -0
  313. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_3d.provenance.json +0 -0
  314. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  315. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_f4_logs.provenance.json +0 -0
  316. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_f4_lwd.las.gz +0 -0
  317. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_surfaces.json.gz +0 -0
  318. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  319. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  320. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  321. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  322. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  323. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  324. {gea_program-0.74.0 → gea_program-0.75.1}/gea/reference/volve_well_picks.csv.gz +0 -0
  325. {gea_program-0.74.0 → gea_program-0.75.1}/gea/report_history.py +0 -0
  326. {gea_program-0.74.0 → gea_program-0.75.1}/gea/rig_state.py +0 -0
  327. {gea_program-0.74.0 → gea_program-0.75.1}/gea/rock_inventory.py +0 -0
  328. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sample_record.py +0 -0
  329. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sample_well_profile.csv +0 -0
  330. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sbom.py +0 -0
  331. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seedlink.py +0 -0
  332. {gea_program-0.74.0 → gea_program-0.75.1}/gea/segy.py +0 -0
  333. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic.py +0 -0
  334. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_array.py +0 -0
  335. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_assoc.py +0 -0
  336. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_detect.py +0 -0
  337. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_film.py +0 -0
  338. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_harmonic.py +0 -0
  339. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_qc.py +0 -0
  340. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_response.py +0 -0
  341. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_signature.py +0 -0
  342. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_track.py +0 -0
  343. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_uncertainty.py +0 -0
  344. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_unlisted.py +0 -0
  345. {gea_program-0.74.0 → gea_program-0.75.1}/gea/seismic_watch.py +0 -0
  346. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sensor_swap.py +0 -0
  347. {gea_program-0.74.0 → gea_program-0.75.1}/gea/series_cache.py +0 -0
  348. {gea_program-0.74.0 → gea_program-0.75.1}/gea/service_life.py +0 -0
  349. {gea_program-0.74.0 → gea_program-0.75.1}/gea/setup_checklist.py +0 -0
  350. {gea_program-0.74.0 → gea_program-0.75.1}/gea/shapefile.py +0 -0
  351. {gea_program-0.74.0 → gea_program-0.75.1}/gea/shell.py +0 -0
  352. {gea_program-0.74.0 → gea_program-0.75.1}/gea/shut_in.py +0 -0
  353. {gea_program-0.74.0 → gea_program-0.75.1}/gea/simulation.py +0 -0
  354. {gea_program-0.74.0 → gea_program-0.75.1}/gea/site_map.py +0 -0
  355. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sla_report.py +0 -0
  356. {gea_program-0.74.0 → gea_program-0.75.1}/gea/slip.py +0 -0
  357. {gea_program-0.74.0 → gea_program-0.75.1}/gea/soak.py +0 -0
  358. {gea_program-0.74.0 → gea_program-0.75.1}/gea/sra.py +0 -0
  359. {gea_program-0.74.0 → gea_program-0.75.1}/gea/store_forward.py +0 -0
  360. {gea_program-0.74.0 → gea_program-0.75.1}/gea/strata_join.py +0 -0
  361. {gea_program-0.74.0 → gea_program-0.75.1}/gea/structure.py +0 -0
  362. {gea_program-0.74.0 → gea_program-0.75.1}/gea/supervisor.py +0 -0
  363. {gea_program-0.74.0 → gea_program-0.75.1}/gea/survey_cmd.py +0 -0
  364. {gea_program-0.74.0 → gea_program-0.75.1}/gea/survey_view.py +0 -0
  365. {gea_program-0.74.0 → gea_program-0.75.1}/gea/telemetry.py +0 -0
  366. {gea_program-0.74.0 → gea_program-0.75.1}/gea/timestamp.py +0 -0
  367. {gea_program-0.74.0 → gea_program-0.75.1}/gea/tls.py +0 -0
  368. {gea_program-0.74.0 → gea_program-0.75.1}/gea/tool_catalog.py +0 -0
  369. {gea_program-0.74.0 → gea_program-0.75.1}/gea/tool_library.py +0 -0
  370. {gea_program-0.74.0 → gea_program-0.75.1}/gea/tool_taxonomy.py +0 -0
  371. {gea_program-0.74.0 → gea_program-0.75.1}/gea/torque_drag.py +0 -0
  372. {gea_program-0.74.0 → gea_program-0.75.1}/gea/transient.py +0 -0
  373. {gea_program-0.74.0 → gea_program-0.75.1}/gea/tray.py +0 -0
  374. {gea_program-0.74.0 → gea_program-0.75.1}/gea/updates.py +0 -0
  375. {gea_program-0.74.0 → gea_program-0.75.1}/gea/vibration.py +0 -0
  376. {gea_program-0.74.0 → gea_program-0.75.1}/gea/web/reader.html +0 -0
  377. {gea_program-0.74.0 → gea_program-0.75.1}/gea/well3d.py +0 -0
  378. {gea_program-0.74.0 → gea_program-0.75.1}/gea/well_assembler.py +0 -0
  379. {gea_program-0.74.0 → gea_program-0.75.1}/gea/well_records.py +0 -0
  380. {gea_program-0.74.0 → gea_program-0.75.1}/gea/well_report.py +0 -0
  381. {gea_program-0.74.0 → gea_program-0.75.1}/gea/well_test_validation.py +0 -0
  382. {gea_program-0.74.0 → gea_program-0.75.1}/gea/wellplan.py +0 -0
  383. {gea_program-0.74.0 → gea_program-0.75.1}/gea/wellpos.py +0 -0
  384. {gea_program-0.74.0 → gea_program-0.75.1}/gea/winident.py +0 -0
  385. {gea_program-0.74.0 → gea_program-0.75.1}/gea/wits0.py +0 -0
  386. {gea_program-0.74.0 → gea_program-0.75.1}/gea/witsml.py +0 -0
  387. {gea_program-0.74.0 → gea_program-0.75.1}/gea/workspace.py +0 -0
  388. {gea_program-0.74.0 → gea_program-0.75.1}/gea/workspaces.py +0 -0
  389. {gea_program-0.74.0 → gea_program-0.75.1}/gea/xlsx_read.py +0 -0
  390. {gea_program-0.74.0 → gea_program-0.75.1}/gea_program.egg-info/dependency_links.txt +0 -0
  391. {gea_program-0.74.0 → gea_program-0.75.1}/gea_program.egg-info/entry_points.txt +0 -0
  392. {gea_program-0.74.0 → gea_program-0.75.1}/gea_program.egg-info/requires.txt +0 -0
  393. {gea_program-0.74.0 → gea_program-0.75.1}/gea_program.egg-info/top_level.txt +0 -0
  394. {gea_program-0.74.0 → gea_program-0.75.1}/setup.cfg +0 -0
  395. {gea_program-0.74.0 → gea_program-0.75.1}/tests/test_acceptance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gea-program
3
- Version: 0.74.0
3
+ Version: 0.75.1
4
4
  Summary: GEA-Program: downhole gauge monitoring - canonical measurement records, quality rules, drift evaluation with a re-fit change log, well-test validation, alarm management, model cards, store-and-forward, monthly SLA measurement, FAT/SAT protocols and a dashboard, generated from live objects in the client's reporting outline.
5
5
  Author: Daniel T. Murphy
6
6
  License: MPL-2.0
@@ -463,6 +463,29 @@ keeps them byte for byte and makes the array and electrode stations of
463
463
  them. `gea help recorder`; the box's README has the parts, the wiring and
464
464
  the clock.
465
465
 
466
+ ### GEA-VPN: the programs on separate computers, one private network
467
+
468
+ The control panel, each pad's recorder, the operator's window and the
469
+ client's reader run on separate computers in separate places. GEA-VPN
470
+ (since 0.75.0; the Administration tab's card, `gea help vpn`) joins them
471
+ into one private WireGuard network over the internet: the office reaches
472
+ every box at a fixed private address (`http://10.77.0.3:8780`) as if it
473
+ sat on the pad's cable - the pull, the live monitor - and nothing else on
474
+ the internet can see any of them. One hub is needed, a small Debian or
475
+ Ubuntu server of the site's own with a public address (UDP 51820); every
476
+ member connects out to it and it forwards between the office and each
477
+ member - members never see one another. GEA makes every key and writes
478
+ every configuration: enter the hub's address, **Make the network**, copy
479
+ `gea-vpn-hub.zip` to the hub and run `sudo sh install-hub.sh`; install
480
+ WireGuard on the office PC from wireguard.com and import the office's
481
+ `gea-vpn.conf`; add a member per computer and hand it its zip (a box's
482
+ holds `vpn.sh`, run once on the Pi; a window imports its `gea-vpn.conf`),
483
+ then run the hub installer again. The keys stay in
484
+ `keys\vpn\network.json`, owner-only, in the backup and never in the
485
+ dossier. From a terminal: `gea vpn --workspace C:\site --create
486
+ vpn.pad3site.com`, `gea vpn --workspace C:\site --add "Pad 3 box" --kind
487
+ box`, `gea vpn --workspace C:\site`.
488
+
466
489
  ### The image: drill-bit seismic from the ground
467
490
 
468
491
  While a well is drilled the program knows where the bit is - the survey and
@@ -757,7 +780,7 @@ unsigned, the key hand-overs - and shows the reports as they were signed. A
757
780
  reader made from the workspace knows the site's key fingerprints, so it
758
781
  says whether a dossier is the site's; it allows no network request, and it
759
782
  leaves the trusted time-stamp to `verify_dossier.py`. For a client who wants
760
- it installed, every release also carries `GEA-Reader-Setup-0.74.0.exe`: GEA
783
+ it installed, every release also carries `GEA-Reader-Setup-0.75.1.exe`: GEA
761
784
  Reader as a Windows program of its own - its own icon and taskbar button,
762
785
  Check with GEA Reader on a dossier's right-click menu, fingerprints the client
763
786
  saves kept on that computer, nothing of GEA-Program. `gea help reader`.
@@ -1522,7 +1545,7 @@ attaches the zips to the GitHub release of that tag.
1522
1545
 
1523
1546
  ### The Windows installer
1524
1547
 
1525
- Every release also carries `GEA-Program-Setup-0.74.0.exe` (for 0.74.0): the
1548
+ Every release also carries `GEA-Program-Setup-0.75.1.exe` (for 0.75.1): the
1526
1549
  same Windows kit installed like any Windows program, for the signed-in user
1527
1550
  alone and with no administrator rights (`%LOCALAPPDATA%\Programs\GEA-Program`).
1528
1551
  It asks for the site workspace (an existing one such as `C:\site` is used as
@@ -1541,7 +1564,7 @@ it, removes the program and leaves the workspace. Silent, for a site's
1541
1564
  software deployment:
1542
1565
 
1543
1566
  ```
1544
- GEA-Program-Setup-0.74.0.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"
1567
+ GEA-Program-Setup-0.75.1.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"
1545
1568
  ```
1546
1569
 
1547
1570
  The setup is built by Inno Setup 6 from `tools/installer.iss` in the same
@@ -424,6 +424,29 @@ keeps them byte for byte and makes the array and electrode stations of
424
424
  them. `gea help recorder`; the box's README has the parts, the wiring and
425
425
  the clock.
426
426
 
427
+ ### GEA-VPN: the programs on separate computers, one private network
428
+
429
+ The control panel, each pad's recorder, the operator's window and the
430
+ client's reader run on separate computers in separate places. GEA-VPN
431
+ (since 0.75.0; the Administration tab's card, `gea help vpn`) joins them
432
+ into one private WireGuard network over the internet: the office reaches
433
+ every box at a fixed private address (`http://10.77.0.3:8780`) as if it
434
+ sat on the pad's cable - the pull, the live monitor - and nothing else on
435
+ the internet can see any of them. One hub is needed, a small Debian or
436
+ Ubuntu server of the site's own with a public address (UDP 51820); every
437
+ member connects out to it and it forwards between the office and each
438
+ member - members never see one another. GEA makes every key and writes
439
+ every configuration: enter the hub's address, **Make the network**, copy
440
+ `gea-vpn-hub.zip` to the hub and run `sudo sh install-hub.sh`; install
441
+ WireGuard on the office PC from wireguard.com and import the office's
442
+ `gea-vpn.conf`; add a member per computer and hand it its zip (a box's
443
+ holds `vpn.sh`, run once on the Pi; a window imports its `gea-vpn.conf`),
444
+ then run the hub installer again. The keys stay in
445
+ `keys\vpn\network.json`, owner-only, in the backup and never in the
446
+ dossier. From a terminal: `gea vpn --workspace C:\site --create
447
+ vpn.pad3site.com`, `gea vpn --workspace C:\site --add "Pad 3 box" --kind
448
+ box`, `gea vpn --workspace C:\site`.
449
+
427
450
  ### The image: drill-bit seismic from the ground
428
451
 
429
452
  While a well is drilled the program knows where the bit is - the survey and
@@ -718,7 +741,7 @@ unsigned, the key hand-overs - and shows the reports as they were signed. A
718
741
  reader made from the workspace knows the site's key fingerprints, so it
719
742
  says whether a dossier is the site's; it allows no network request, and it
720
743
  leaves the trusted time-stamp to `verify_dossier.py`. For a client who wants
721
- it installed, every release also carries `GEA-Reader-Setup-0.74.0.exe`: GEA
744
+ it installed, every release also carries `GEA-Reader-Setup-0.75.1.exe`: GEA
722
745
  Reader as a Windows program of its own - its own icon and taskbar button,
723
746
  Check with GEA Reader on a dossier's right-click menu, fingerprints the client
724
747
  saves kept on that computer, nothing of GEA-Program. `gea help reader`.
@@ -1483,7 +1506,7 @@ attaches the zips to the GitHub release of that tag.
1483
1506
 
1484
1507
  ### The Windows installer
1485
1508
 
1486
- Every release also carries `GEA-Program-Setup-0.74.0.exe` (for 0.74.0): the
1509
+ Every release also carries `GEA-Program-Setup-0.75.1.exe` (for 0.75.1): the
1487
1510
  same Windows kit installed like any Windows program, for the signed-in user
1488
1511
  alone and with no administrator rights (`%LOCALAPPDATA%\Programs\GEA-Program`).
1489
1512
  It asks for the site workspace (an existing one such as `C:\site` is used as
@@ -1502,7 +1525,7 @@ it, removes the program and leaves the workspace. Silent, for a site's
1502
1525
  software deployment:
1503
1526
 
1504
1527
  ```
1505
- GEA-Program-Setup-0.74.0.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"
1528
+ GEA-Program-Setup-0.75.1.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"
1506
1529
  ```
1507
1530
 
1508
1531
  The setup is built by Inno Setup 6 from `tools/installer.iss` in the same
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.74.0"
126
+ __version__ = "0.75.1"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -1023,6 +1023,19 @@ def main(argv=None) -> int:
1023
1023
  p_up.add_argument("--install", type=str, default=None, help="run the fetched GEA-Program-Setup of this version (Windows): the control panel stops and comes back")
1024
1024
  p_up.add_argument("--actor", type=str, default="cli")
1025
1025
 
1026
+ p_vp = sub.add_parser("vpn", help="GEA-VPN: the private WireGuard network the programs talk over - make it, add and remove members, write the hub installer and every member's configuration (the Administration card does the same by clicking)")
1027
+ p_vp.add_argument("--workspace", type=str, default=None, help="the workspace (C:\\site); keys in keys\\vpn\\network.json, files under exports\\vpn")
1028
+ p_vp.add_argument("--create", type=str, default=None, metavar="HOST", help="make the network with the hub at this public name or address (vpn.pad3site.com or 203.0.113.10); the office member is made with it")
1029
+ p_vp.add_argument("--port", type=int, default=None, help="with --create or --hub: the hub's UDP port (default 51820)")
1030
+ p_vp.add_argument("--subnet", type=str, default=None, help="with --create: the private subnet (default 10.77.0.0/24)")
1031
+ p_vp.add_argument("--hub", type=str, default=None, metavar="HOST", help="change the hub's public name or address; every configuration is written again")
1032
+ p_vp.add_argument("--add", type=str, default=None, metavar="NAME", help="add a member of this name (Pad 3 box) of the --kind")
1033
+ p_vp.add_argument("--kind", type=str, default="box", help="with --add: office, box, operator, reader or other (default box)")
1034
+ p_vp.add_argument("--remove", type=str, default=None, metavar="ID", help="remove the member of this id (pad-3-box); run the hub installer again after")
1035
+ p_vp.add_argument("--bundles", action="store_true", help="write the hub installer and every member's configuration again")
1036
+ p_vp.add_argument("--json", action="store_true")
1037
+ p_vp.add_argument("--actor", type=str, default="cli")
1038
+
1026
1039
  p_nt = sub.add_parser("notify", help="notification rules: validate the configuration, send a test message, show the delivery log")
1027
1040
  p_nt.add_argument("--workspace", type=str, default=None)
1028
1041
  p_nt.add_argument("--test", type=str, default=None, help="send a test message through this channel name")
@@ -2358,6 +2371,40 @@ def main(argv=None) -> int:
2358
2371
  return 0
2359
2372
  from .doctor import update as _update
2360
2373
  return _update(check_only=a.check, extras=a.extras, as_json=a.json)
2374
+ elif a.cmd == "vpn":
2375
+ import json as _json
2376
+ from . import vpn as VP
2377
+ from .workspace import Workspace
2378
+ wsp = a.workspace or os.environ.get("GEA_WORKSPACE")
2379
+ if not wsp:
2380
+ raise SystemExit("vpn needs --workspace C:\\site")
2381
+ ws = Workspace(wsp)
2382
+ try:
2383
+ if a.create:
2384
+ VP.create(ws, a.actor, a.create, hub_port=a.port or VP.DEFAULT_PORT, subnet=a.subnet or VP.DEFAULT_SUBNET)
2385
+ print(f"the network is made; hub {a.create}:{a.port or VP.DEFAULT_PORT}")
2386
+ if a.hub:
2387
+ VP.set_hub(ws, a.actor, a.hub, a.port)
2388
+ print(f"the hub is now {a.hub}")
2389
+ if a.add:
2390
+ m = VP.add_member(ws, a.actor, a.add, a.kind)
2391
+ print(f"added {m['name']} ({m['kind']}) at {m['address']}: gea-vpn-{m['id']}.zip; run the hub installer again")
2392
+ if a.remove:
2393
+ m = VP.remove_member(ws, a.actor, a.remove)
2394
+ print(f"removed {m['name']} ({m['address']}); run the hub installer again")
2395
+ if a.bundles:
2396
+ b = VP.write_bundles(ws, a.actor)
2397
+ print("written: " + ", ".join(sorted(b)))
2398
+ except VP.VpnError as e:
2399
+ raise SystemExit(f"vpn: {e}")
2400
+ if a.json:
2401
+ print(_json.dumps(VP.status(ws, probes=False), indent=1))
2402
+ else:
2403
+ print(VP.text(ws))
2404
+ st = VP.status(ws, probes=False)
2405
+ if st["network"]:
2406
+ print(f" files: {st['exports_dir']}")
2407
+ return 0
2361
2408
  elif a.cmd == "checklist":
2362
2409
  import json as _json
2363
2410
  from . import setup_checklist as _SC
@@ -15913,7 +15913,8 @@ def section_dr_field_recorder(tmp: str) -> None:
15913
15913
  "started": st["running"] is True and st["pid"] and st["already_running"] is False and st["health"]["station"] == "SIM" and st["health"]["site"] == "Pad 3 sim",
15914
15914
  "live": live is not None and live["recording"] is True and len(live["channels"]) == 6,
15915
15915
  "stopped": sp["stopped"] is True and sp["running"] is False and RL.health(port, timeout=0.5) is None,
15916
- "closed": len(man3["files"]) == 6 and all(Path(RL.sim_dir(ws3), "data", e["file"]).is_file() for e in man3["files"]),
15916
+ # six channels, a file each - two each when the run crossed a minute (the simulated box splits its files every minute)
15917
+ "closed": len({e["sensor_id"] for e in man3["files"]}) == 6 and len(man3["files"]) in (6, 12) and all(Path(RL.sim_dir(ws3), "data", e["file"]).is_file() for e in man3["files"]),
15917
15918
  "pulled": [s_["kind"] for s_ in r4["stations"]] == ["array", "electrodes"] and len(r4["stations"][0]["sensors"]) == 4,
15918
15919
  "monitor_url": RL.monitor_url("192.168.1.50") == "http://192.168.1.50:8780/live" and RL.monitor_url("http://192.168.1.50:8780/") == "http://192.168.1.50:8780/live",
15919
15920
  "audit": "field.sim_start" in acts3 and "field.sim_stop" in acts3,
@@ -16065,6 +16066,224 @@ def section_ds_updates(tmp: str) -> None:
16065
16066
  "--install, and the help page carry it" + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
16066
16067
 
16067
16068
 
16069
+ def section_dt_vpn(tmp: str) -> None:
16070
+ """Section DT - GEA-VPN: the programs on separate computers in separate places joined by a WireGuard network GEA
16071
+ manages. The key agreement against RFC 7748's test vectors (and against `wg pubkey` where the tools are here);
16072
+ a network made, members added and removed with the refusals (a second office, a name twice, a public subnet,
16073
+ a bad host); every configuration's contents (what each kind may reach, the pre-shared key, the keepalive, the
16074
+ hub's forwarding rules); the installer and member bundles, read back by wg-quick where it is installed and by
16075
+ sh -n; the private keys in one owner-only file that the backup carries; the routes, the card, the command and
16076
+ the help."""
16077
+ import base64
16078
+ import http.cookiejar
16079
+ import io
16080
+ import shutil
16081
+ import stat
16082
+ import subprocess
16083
+ import urllib.error
16084
+ import urllib.request
16085
+ import zipfile
16086
+ from . import backup as BK
16087
+ from . import helplib as H
16088
+ from . import vpn as VP
16089
+ from . import x25519 as X
16090
+ from .service import Service, Users
16091
+ from .workspace import Workspace
16092
+ pkg = Path(__file__).parent
16093
+ d = Path(tmp, "dt"); d.mkdir()
16094
+ # -- DT1 the key agreement -----------------------------------------------------------------------------------------------
16095
+ k1 = bytes.fromhex("a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4")
16096
+ u1 = bytes.fromhex("e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c")
16097
+ r1 = "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552"
16098
+ a_priv = bytes.fromhex("77076d0a7318a57d3c16c17251b26645df4c2f87ebc0992ab177fba51db92c2a")
16099
+ b_priv = bytes.fromhex("5dab087e624a8a4b79e17f8b83800ee66f3bb1292618b6fd1c2f8b27ff88e0eb")
16100
+ a_pub = "8520f0098930a754748b7ddcb43ef75a0dbf3a0d26381af4eba4a98eaa9b4e6a"
16101
+ b_pub = "de9edb7d7b7dc1b4d35b61c2ece435373f8343c85b78674dadfc7e146f882b4f"
16102
+ shared = "4a5d9d5ba4ce2de1728e3bf480350f25e07e21c947d19e3376f09b3c1e161742"
16103
+ pub_a = X.public_key(a_priv); pub_b = X.public_key(b_priv)
16104
+ wg = shutil.which("wg") if os.name != "nt" else None
16105
+ wg_same = None
16106
+ if wg:
16107
+ try:
16108
+ wg_same = all(subprocess.run([wg, "pubkey"], input=pv, capture_output=True, text=True, timeout=5).stdout.strip() == pb
16109
+ for pv, pb in [X.keypair_b64() for _ in range(3)])
16110
+ except Exception:
16111
+ wg_same = False
16112
+ kp = X.keypair_b64()
16113
+ c1 = {
16114
+ "rfc_vector": X.x25519(k1, u1).hex() == r1,
16115
+ "rfc_alice_bob": pub_a.hex() == a_pub and pub_b.hex() == b_pub and X.x25519(a_priv, pub_b).hex() == shared and X.x25519(b_priv, pub_a).hex() == shared,
16116
+ "keys_32": len(base64.b64decode(kp[0])) == 32 and len(base64.b64decode(kp[1])) == 32 and X.public_from_b64(kp[0]) == kp[1],
16117
+ "clamped": (base64.b64decode(kp[0])[0] & 7) == 0 and (base64.b64decode(kp[0])[31] & 0xC0) == 0x40,
16118
+ "wg_agrees": wg_same in (None, True),
16119
+ "two_differ": X.keypair_b64()[0] != X.keypair_b64()[0] and len(X.preshared_b64()) == 44,
16120
+ }
16121
+ failed1 = [k for k, v in c1.items() if not v]
16122
+ ok(not failed1,
16123
+ "DT1 the program's own X25519 reproduces RFC 7748's test vector and its Alice/Bob exchange, makes clamped 32-byte key pairs whose public key "
16124
+ + ("the WireGuard tool here agrees with" if wg else "the RFC's ladder gives") + (f" [failed: {', '.join(failed1)}]" if failed1 else ""))
16125
+ # -- DT2 the network: made, members, refusals, the files ---------------------------------------------------------------
16126
+ ws = Workspace.create(str(d / "ws"), "Pad 3 vpn", actor="tester")
16127
+ refusals = {}
16128
+ for nm, fn in (("host", lambda: VP.create(ws, "tester", "not a host!")), ("subnet", lambda: VP.create(ws, "tester", "vpn.example.com", subnet="8.8.8.0/24")),
16129
+ ("member_before", lambda: VP.add_member(ws, "tester", "Box", "box"))):
16130
+ try:
16131
+ fn(); refusals[nm] = False
16132
+ except VP.VpnError:
16133
+ refusals[nm] = True
16134
+ net = VP.create(ws, "tester", "vpn.example.com")
16135
+ box = VP.add_member(ws, "tester", "Pad 3 box", "box")
16136
+ rd = VP.add_member(ws, "tester", "Client reader", "reader")
16137
+ for nm, fn in (("twice", lambda: VP.create(ws, "tester", "vpn.example.com")), ("name_twice", lambda: VP.add_member(ws, "tester", "pad 3 BOX", "box")),
16138
+ ("second_office", lambda: VP.add_member(ws, "tester", "Office 2", "office")), ("kind", lambda: VP.add_member(ws, "tester", "X", "laptop")),
16139
+ ("remove_office", lambda: VP.remove_member(ws, "tester", "office")), ("remove_none", lambda: VP.remove_member(ws, "tester", "nobody"))):
16140
+ try:
16141
+ fn(); refusals[nm] = False
16142
+ except VP.VpnError:
16143
+ refusals[nm] = True
16144
+ raw = json.loads(Path(ws.path, VP.FILE).read_text(encoding="utf-8"))
16145
+ office = next(m for m in raw["members"] if m["kind"] == "office")
16146
+ bm = next(m for m in raw["members"] if m["id"] == box["id"])
16147
+ mc_box = VP.member_conf(raw, bm); mc_off = VP.member_conf(raw, office); hc = VP.hub_conf(raw)
16148
+ mode_ok = True
16149
+ if os.name != "nt":
16150
+ mode_ok = stat.S_IMODE(os.stat(Path(ws.path, VP.FILE)).st_mode) == 0o600
16151
+ st = VP.status(ws, probes=False)
16152
+ pub_json = json.dumps(st)
16153
+ c2 = {
16154
+ "refusals": all(refusals.values()) and len(refusals) == 9,
16155
+ "addresses": office["address"] == "10.77.0.2" and box["address"] == "10.77.0.3" and rd["address"] == "10.77.0.4" and raw["hub"]["address"] == "10.77.0.1",
16156
+ "keys_private": mode_ok and all(m.get("private_key") and m.get("preshared_key") for m in raw["members"]) and raw["hub"]["private_key"]
16157
+ and all(X.public_from_b64(m["private_key"]) == m["public_key"] for m in raw["members"]) and X.public_from_b64(raw["hub"]["private_key"]) == raw["hub"]["public_key"],
16158
+ "status_no_keys": "private_key" not in pub_json and "preshared_key" not in pub_json and len(st["network"]["members"]) == 3,
16159
+ "box_conf": f"PrivateKey = {bm['private_key']}" in mc_box and f"PresharedKey = {bm['preshared_key']}" in mc_box and "AllowedIPs = 10.77.0.1/32, 10.77.0.2/32" in mc_box
16160
+ and "Endpoint = vpn.example.com:51820" in mc_box and "PersistentKeepalive = 25" in mc_box and f"PublicKey = {raw['hub']['public_key']}" in mc_box,
16161
+ "office_conf": "AllowedIPs = 10.77.0.0/24" in mc_off and "Address = 10.77.0.2/32" in mc_off,
16162
+ "hub_conf": "ListenPort = 51820" in hc and hc.count("[Peer]") == 3 and all(f"PublicKey = {m['public_key']}" in hc and f"AllowedIPs = {m['address']}/32" in hc for m in raw["members"])
16163
+ and "-s 10.77.0.2/32 -j ACCEPT" in hc and "-d 10.77.0.2/32 -j ACCEPT" in hc and "-o gea-vpn -j DROP" in hc and "PostDown" in hc and raw["hub"]["private_key"] in hc,
16164
+ "audit": {"vpn.create", "vpn.member.add"} <= {a["action"] for a in ws.audit_log()},
16165
+ }
16166
+ failed2 = [k for k, v in c2.items() if not v]
16167
+ ok(not failed2,
16168
+ "DT2 a network made with its office; a box and a reader added at the next addresses; nine refusals (a bad host, a public subnet, a member before "
16169
+ "a network, a second network, a name twice, a second office, an unknown kind, removing the office, removing nobody); every private key in one "
16170
+ "owner-only file and nowhere in the status; a box reaches the hub and the office only, the office the whole subnet, every member with a "
16171
+ "pre-shared key and a keepalive; the hub lists every member and forwards office<->member only" + (f" [failed: {', '.join(failed2)}]" if failed2 else ""))
16172
+ # -- DT3 the bundles -----------------------------------------------------------------------------------------------------
16173
+ written = VP.write_bundles(ws, "tester")
16174
+ root = Path(ws.path, "exports", "vpn")
16175
+ hub_zip = root / "gea-vpn-hub.zip"; box_zip = root / f"gea-vpn-{box['id']}.zip"; rd_zip = root / f"gea-vpn-{rd['id']}.zip"
16176
+ with zipfile.ZipFile(hub_zip) as z:
16177
+ hub_names = sorted(n.split("/", 1)[1] for n in z.namelist()); install_sh = z.read("gea-vpn-hub/install-hub.sh").decode()
16178
+ with zipfile.ZipFile(box_zip) as z:
16179
+ box_names = sorted(n.split("/", 1)[1] for n in z.namelist()); box_conf_z = z.read(f"gea-vpn-{box['id']}/gea-vpn.conf").decode(); vpn_sh = z.read(f"gea-vpn-{box['id']}/vpn.sh").decode()
16180
+ with zipfile.ZipFile(rd_zip) as z:
16181
+ rd_names = sorted(n.split("/", 1)[1] for n in z.namelist())
16182
+ sh = shutil.which("sh") if os.name != "nt" else None # the scripts run on Linux hosts; Git's sh.exe on a Windows machine is not that
16183
+ sh_ok = None
16184
+ if sh:
16185
+ try:
16186
+ sh_ok = all(subprocess.run([sh, "-n", str(root / f)], capture_output=True, timeout=10).returncode == 0 for f in ("hub/install-hub.sh", f"{box['id']}/vpn.sh"))
16187
+ except (OSError, subprocess.SubprocessError):
16188
+ sh_ok = None
16189
+ wgq = shutil.which("wg-quick") if os.name != "nt" else None
16190
+ strip_ok = None
16191
+ if wgq:
16192
+ try:
16193
+ strip_ok = all(subprocess.run([wgq, "strip", str(root / f)], capture_output=True, timeout=10).returncode == 0 for f in ("hub/gea-vpn.conf", f"{box['id']}/gea-vpn.conf", "office/gea-vpn.conf"))
16194
+ except (OSError, subprocess.SubprocessError):
16195
+ strip_ok = None
16196
+ removed = VP.remove_member(ws, "tester", rd["id"])
16197
+ rd_gone_at_once = not rd_zip.exists() and not (root / rd["id"]).exists()
16198
+ VP.write_bundles(ws, "tester")
16199
+ after = VP.status(ws, probes=False)
16200
+ hc2 = (root / "hub" / "gea-vpn.conf").read_text(encoding="utf-8")
16201
+ c3 = {
16202
+ "written": set(written) == {"hub", "office", box["id"], rd["id"]},
16203
+ "hub_zip": hub_names == ["README.txt", "gea-vpn.conf", "install-hub.sh"] and "wg syncconf" in install_sh and "PORT=51820" in install_sh and "$PORT/udp" in install_sh and "ip_forward" in install_sh and "IFACE=gea-vpn" in install_sh and "wg-quick@$IFACE" in install_sh,
16204
+ "box_zip": box_names == ["gea-vpn.conf", "vpn.sh"] and box_conf_z == (root / box["id"] / "gea-vpn.conf").read_text(encoding="utf-8") and "IFACE=gea-vpn" in vpn_sh and "wg-quick@$IFACE" in vpn_sh and "wireguard" in vpn_sh,
16205
+ "reader_zip": rd_names == ["README.txt", "gea-vpn.conf"],
16206
+ "sh_n": sh_ok in (None, True),
16207
+ "wg_quick_strip": strip_ok in (None, True),
16208
+ "removed": removed["id"] == rd["id"] and rd_gone_at_once and f"gea-vpn-{rd['id']}.zip" not in after["bundles"] and not rd_zip.exists() and rd["public_key"] not in hc2
16209
+ and len(after["network"]["members"]) == 2 and box["public_key"] in hc2,
16210
+ }
16211
+ failed3 = [k for k, v in c3.items() if not v]
16212
+ ok(not failed3,
16213
+ "DT3 the hub bundle (the configuration, an idempotent installer that opens 51820/udp, turns forwarding on and syncs a running tunnel, a README) "
16214
+ "and a bundle per member (a box's with vpn.sh, a window's with a README)" + (", read back by wg-quick strip" if wgq else "") + (", sh -n clean" if sh else "")
16215
+ + "; a member removed leaves the hub's configuration and the files without it" + (f" [failed: {', '.join(failed3)}]" if failed3 else ""))
16216
+ # -- DT4 the backup carries the keys; the routes; the card; the command; the help --------------------------------------
16217
+ bk = BK.make(ws.path, str(d / "bk"), actor="tester")
16218
+ with zipfile.ZipFile(bk["path"]) as z:
16219
+ bk_names = z.namelist()
16220
+ Users(str(Path(ws.path, "users.json"))).add("adm", "admin-pass-123", "admin")
16221
+ Users(str(Path(ws.path, "users.json"))).add("op", "op-pass-123", "operator")
16222
+ svc = Service(ws.path, port=0, scheduler=False).start()
16223
+ base = svc.url.rstrip("/")
16224
+
16225
+ def opener_():
16226
+ return urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar()))
16227
+
16228
+ def call(op, method, path, body=None, raw_=False):
16229
+ rq = urllib.request.Request(base + path, method=method, data=(json.dumps(body).encode() if body is not None else None))
16230
+ rq.add_header("Content-Type", "application/json"); rq.add_header("X-GEA-Action", "1")
16231
+ try:
16232
+ with op.open(rq, timeout=60) as rr_:
16233
+ data = rr_.read()
16234
+ return rr_.status, (data if raw_ else json.loads(data)), rr_.headers
16235
+ except urllib.error.HTTPError as e:
16236
+ return e.code, (e.read() if raw_ else json.loads(e.read() or b"{}")), e.headers
16237
+ try:
16238
+ anon = opener_()
16239
+ s_anon, _, _ = call(anon, "GET", "/api/vpn")
16240
+ op_ = opener_(); call(op_, "POST", "/api/login", {"name": "op", "password": "op-pass-123"})
16241
+ s_op, _, _ = call(op_, "GET", "/api/vpn")
16242
+ s_opz, _, _ = call(op_, "GET", "/api/vpn/bundle/gea-vpn-hub.zip", raw_=True)
16243
+ adm = opener_(); call(adm, "POST", "/api/login", {"name": "adm", "password": "admin-pass-123"})
16244
+ s_st, st_, _ = call(adm, "GET", "/api/vpn?probe=0")
16245
+ s_add, added, _ = call(adm, "POST", "/api/vpn/members", {"name": "Operator window", "kind": "operator"})
16246
+ s_dup, _, _ = call(adm, "POST", "/api/vpn/members", {"name": "Operator window", "kind": "operator"})
16247
+ s_bad, _, _ = call(adm, "POST", "/api/vpn/create", {"hub_host": "x"})
16248
+ s_z, zbytes, zh = call(adm, "GET", "/api/vpn/bundle/gea-vpn-operator-window.zip", raw_=True)
16249
+ s_trav, _, _ = call(adm, "GET", "/api/vpn/bundle/..%2fnetwork.json", raw_=True)
16250
+ s_other, _, _ = call(adm, "GET", "/api/vpn/bundle/gea-vpn-nobody.zip", raw_=True)
16251
+ s_rm, _, _ = call(adm, "POST", f"/api/vpn/members/{added.get('id', 'operator-window')}/remove")
16252
+ s_hub, hub_, _ = call(adm, "POST", "/api/vpn/hub", {"host": "203.0.113.10", "port": 51821})
16253
+ s_b, _, _ = call(adm, "POST", "/api/vpn/bundles")
16254
+ finally:
16255
+ svc.stop()
16256
+ zip_ok = False
16257
+ try:
16258
+ with zipfile.ZipFile(io.BytesIO(zbytes)) as z:
16259
+ zip_ok = "gea-vpn-operator-window/gea-vpn.conf" in z.namelist()
16260
+ except Exception:
16261
+ pass
16262
+ app_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
16263
+ main_src = (pkg / "__main__.py").read_text(encoding="utf-8")
16264
+ sec_doc = (pkg / "help" / "IT_SECURITY.md").read_text(encoding="utf-8")
16265
+ final = VP.status(ws, probes=False)
16266
+ c4 = {
16267
+ "backup_keys": "keys/vpn/network.json" in bk_names,
16268
+ "anon_op": s_anon in (401, 403) and s_op == 403 and s_opz == 403,
16269
+ "status": s_st == 200 and st_["network"]["hub"]["host"] == "vpn.example.com" and "private_key" not in json.dumps(st_) and "gea-vpn-hub.zip" in st_["bundles"],
16270
+ "add": s_add == 200 and s_dup == 400 and s_bad == 400,
16271
+ "download": s_z == 200 and zip_ok and "attachment" in str(zh.get("Content-Disposition", "")) and s_trav in (400, 404) and s_other == 404,
16272
+ "remove_hub": s_rm == 200 and s_hub == 200 and s_b == 200 and final["network"]["hub"]["host"] == "203.0.113.10" and final["network"]["hub"]["port"] == 51821
16273
+ and len(final["network"]["members"]) == 2 and "203.0.113.10:51821" in (root / "office" / "gea-vpn.conf").read_text(encoding="utf-8"),
16274
+ "audit_download": {"vpn.bundle.download", "vpn.bundles", "vpn.member.remove", "vpn.hub"} <= {a["action"] for a in ws.audit_log()},
16275
+ "card": all(t_ in app_src for t_ in ("vpnCardLoad", "/api/vpn/create", "/api/vpn/members", "/api/vpn/bundle/", "Make the network", "Add the member", "Make the files again", "gea-vpn-hub.zip")),
16276
+ "cli": all(t_ in main_src for t_ in ('add_parser("vpn"', '"--create"', '"--add"', '"--remove"', '"--bundles"', "VP.add_member")),
16277
+ "help": not H.check("vpn") and any(t_ == "vpn" for t_, _, _ in H.INDEX) and "GEA-VPN" in sec_doc and "51820" in sec_doc and "keys\\vpn\\network.json" in sec_doc,
16278
+ }
16279
+ failed4 = [k for k, v in c4.items() if not v]
16280
+ ok(not failed4,
16281
+ "DT4 the backup carries keys/vpn/network.json; the routes are admin-only (anonymous and operator refused, a bundle too), the status never "
16282
+ "carries a private key, a name twice and a bad host are 400, a bundle downloads as an attachment and is audited, a path outside the folder "
16283
+ "and an unknown bundle are refused, a member removed and the hub changed rewrite every file; the Administration card, the vpn command "
16284
+ "and the help page carry it" + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
16285
+
16286
+
16068
16287
  def main() -> int:
16069
16288
  print("GEA-Program - ACCEPTANCE SUITE (the product gate)")
16070
16289
  # a handle a job subprocess still holds at the end must not turn a finished gate into a traceback on Windows:
@@ -16191,6 +16410,7 @@ def main() -> int:
16191
16410
  section_dq_bit_image(tmp)
16192
16411
  section_dr_field_recorder(tmp)
16193
16412
  section_ds_updates(tmp)
16413
+ section_dt_vpn(tmp)
16194
16414
  if _FAILS:
16195
16415
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
16196
16416
  for f in _FAILS:
@@ -148,6 +148,25 @@ declared extras.
148
148
  every response carries a Content-Security-Policy (`default-src 'self'`,
149
149
  `frame-ancestors 'none'`), `X-Frame-Options: DENY`, `nosniff` and a
150
150
  same-origin referrer policy. The page loads nothing from the internet.
151
+ - **GEA-VPN** (since 0.75.0, `gea help vpn`): the programs run on separate
152
+ computers in separate places and talk over a WireGuard network GEA
153
+ manages. One hub, a Debian or Ubuntu server of the site's own with a
154
+ public address, listens on UDP 51820 and forwards between the office and
155
+ each member (`iptables` FORWARD rule: office to any member and back
156
+ ACCEPT, everything else DROP, so members never reach one another);
157
+ members only connect out (a box behind a cellular modem opens no inbound
158
+ port; a keepalive every 25 s holds the tunnel). Every key pair is made by
159
+ GEA (X25519 by `wg genkey` where it is installed, else the program's own
160
+ RFC 7748 implementation checked against the RFC's vectors in the gate) and
161
+ every member has a pre-shared key besides. The private keys are in
162
+ `keys\vpn\network.json`, owner-only (0600), carried by the backup and
163
+ never by the dossier; a member's configuration file holds that member's
164
+ private key and is handed over by a channel of its own. The hub installer
165
+ and the member files are under `exports\vpn\`; the card's downloads are
166
+ admin-only and audited (`vpn.bundle.download`). The tunnel encrypts and
167
+ addresses; the programs' own sign-in, roles and hashes apply through it
168
+ unchanged, and the box's page is still bound to the pad network and the
169
+ tunnel address only.
151
170
 
152
171
  ## What it records
153
172
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  > GEA-Program-Setup: the program installed for you like any Windows program - a window of its own, an icon in the notification area, Start-menu and desktop shortcuts, an Add/Remove Programs entry, upgrades in place - with no administrator rights and the site's data left where it is.
4
4
 
5
- **The command**: download the setup from the release on GitHub (`GEA-Program-Setup-0.74.0.exe` for this version) and run it. It installs for you alone, under `%LOCALAPPDATA%\Programs\GEA-Program`, and asks for the site workspace - the folder that holds the wells, reports, records, accounts and keys. An existing one such as `C:\site` is used exactly as it is, its name included; for a new one (by default `%LOCALAPPDATA%\GEA-Program\site`) it also asks the site's name, which heads every report, and makes the workspace with it (left empty, the workspace is named after its folder; an administrator renames the site in Administration's site settings). Then the Start menu's **GEA-Program** (and the desktop shortcut) starts the control panel when it is not running and opens it in a window of its own. A newer setup installs over the older one: it stops the control panel, replaces the program, keeps the workspace and the choices made before. For a site's software deployment: `GEA-Program-Setup-0.74.0.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"` (`/SITENAME` names a workspace the setup makes; one that exists keeps its own name). From a terminal: `gea launch` does what the shortcut does, `gea launch --selftest` says what it would do, `gea tray --selftest` checks the icon.
5
+ **The command**: download the setup from the release on GitHub (`GEA-Program-Setup-0.75.1.exe` for this version) and run it. It installs for you alone, under `%LOCALAPPDATA%\Programs\GEA-Program`, and asks for the site workspace - the folder that holds the wells, reports, records, accounts and keys. An existing one such as `C:\site` is used exactly as it is, its name included; for a new one (by default `%LOCALAPPDATA%\GEA-Program\site`) it also asks the site's name, which heads every report, and makes the workspace with it (left empty, the workspace is named after its folder; an administrator renames the site in Administration's site settings). Then the Start menu's **GEA-Program** (and the desktop shortcut) starts the control panel when it is not running and opens it in a window of its own. A newer setup installs over the older one: it stops the control panel, replaces the program, keeps the workspace and the choices made before. For a site's software deployment: `GEA-Program-Setup-0.75.1.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /WORKSPACE="C:\site" /SITENAME="Pad 3" /TASKS="desktopicon"` (`/SITENAME` names a workspace the setup makes; one that exists keeps its own name). From a terminal: `gea launch` does what the shortcut does, `gea launch --selftest` says what it would do, `gea tray --selftest` checks the icon.
6
6
 
7
7
  **The window**: Microsoft Edge's app window (`msedge --app=`: the control panel with no tabs and no address bar, its own button on the taskbar, which shows the program's derrick and pins as GEA-Program, not as Edge), with a browser profile of its own (`%LOCALAPPDATA%\GEA-Program\window`, no extensions; the first time, Edge may say it signed that profile in to your Microsoft account - Got it), else Google Chrome's, else a tab in the default browser. Where the browser keeps no cookies (its settings or a policy block them), the sign-in page says so and the control panel holds the sign-in itself, in the window's address, until the window is closed. The control panel itself runs with no window of its own: `gea supervise` keeps it running in the background, starts it again when it asks for a restart, and writes everything it would have shown in a console - the start, the sign-ins, the jobs, a stop and why - to `records\console.log` in the workspace. No PowerShell or console window opens. The kit's `start-dashboard.cmd` still runs it in a console window, for a site that wants to watch it there.
8
8
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  **The number to check**: Add/Remove Programs lists GEA-Program with the version you installed; `gea doctor --workspace C:\site` ends with `nothing blocks serving`; the Start menu's **Verify GEA-Program** runs the acceptance gate from the installation and ends with `[ACCEPTANCE] OK - N checks passed`. The release's build installs, upgrades and uninstalls the setup silently before it is published, with the control panel running, and checks the workspace untouched afterwards.
20
20
 
21
- **Signed**: when the publisher's code-signing certificate is configured for the build, the setup is signed with it (Authenticode, timestamped) and Windows names the publisher; until then it ships unsigned and Windows SmartScreen says "unknown publisher" (More info, then Run anyway). The build's provenance holds either way: `gh attestation verify GEA-Program-Setup-0.74.0.exe -R Daniel8Murphy0007/GEA-Program`. The uninstaller the setup leaves behind is Inno Setup's own and is not signed separately.
21
+ **Signed**: when the publisher's code-signing certificate is configured for the build, the setup is signed with it (Authenticode, timestamped) and Windows names the publisher; until then it ships unsigned and Windows SmartScreen says "unknown publisher" (More info, then Run anyway). The build's provenance holds either way: `gh attestation verify GEA-Program-Setup-0.75.1.exe -R Daniel8Murphy0007/GEA-Program`. The uninstaller the setup leaves behind is Inno Setup's own and is not signed separately.
22
22
 
23
23
  **What this page will not call a measurement**: the icon's running or stopped is whether the control panel's port answers on this computer, read every three seconds - not whether every patch, job or report behind it is healthy (Home and the Verification page say that).
24
24
 
@@ -4,7 +4,7 @@
4
4
 
5
5
  **The command**: Reports tab -> Signed dossiers -> **Download GEA Reader for your clients**, or `gea reader --workspace C:\site --out C:\site\GEA-Reader.html`. Send the file once, by any channel (email, a USB stick, the client's portal); send the site's key fingerprint separately - by phone, email or in the contract - as for any dossier. The client opens GEA-Reader.html, drops a dossier on it (or chooses it), and reads the verdict: **Valid** (signed with the site's key, or with a newer key the site's key handed over to), **Valid, but signed by a key you were not given**, or **Not valid** with the reason. Below the verdict: each check, the signer's fingerprint, the key hand-overs, and every file the dossier holds - a report opens in the page as it was signed, a PDF in the browser's own viewer, a table or a log as text, and any file can be saved. A dossier that lists source records by hash without carrying them lets the client choose their copies and checks each against its hash.
6
6
 
7
- **The program**: for a client who wants it installed, **GEA-Reader-Setup-0.74.0.exe** on the release page installs GEA Reader as a Windows program of its own, for the signed-in user, with no administrator rights and nothing of GEA-Program: GEA Reader in the Start menu and on the desktop with its own icon (a white page with a green seal, on blue) and its own taskbar button, and **Check with GEA Reader** on the right-click menu of .zip files (on Windows 11, under Show more options), which opens the dossier at once. It opens the same page in Microsoft Edge's app window with a browser profile of its own (`%LOCALAPPDATA%\GEA-Reader\window`), so a fingerprint the client saves - **Remember this fingerprint**, with whose it is - stays on that computer, and every later dossier signed with that key reads **Valid** with the name the client gave it. `GEA-Reader-portable-0.74.0.zip` is the same program to unzip and run (`GEA-Reader.cmd`). `python\python.exe -m gea_reader --selftest`, in the installed folder, checks the page, the icon, the taskbar identity and the hand-over of a dossier without opening a window.
7
+ **The program**: for a client who wants it installed, **GEA-Reader-Setup-0.75.1.exe** on the release page installs GEA Reader as a Windows program of its own, for the signed-in user, with no administrator rights and nothing of GEA-Program: GEA Reader in the Start menu and on the desktop with its own icon (a white page with a green seal, on blue) and its own taskbar button, and **Check with GEA Reader** on the right-click menu of .zip files (on Windows 11, under Show more options), which opens the dossier at once. It opens the same page in Microsoft Edge's app window with a browser profile of its own (`%LOCALAPPDATA%\GEA-Reader\window`), so a fingerprint the client saves - **Remember this fingerprint**, with whose it is - stays on that computer, and every later dossier signed with that key reads **Valid** with the name the client gave it. `GEA-Reader-portable-0.75.1.zip` is the same program to unzip and run (`GEA-Reader.cmd`). `python\python.exe -m gea_reader --selftest`, in the installed folder, checks the page, the icon, the taskbar identity and the hand-over of a dossier without opening a window.
8
8
 
9
9
  **What it writes**: the one file, GEA-Reader.html (about 40 KB): the checks, written in the page's own script - Ed25519 (RFC 8032) in its own arithmetic, SHA-256 from the browser - and, when made from a workspace, the site's name and the fingerprints of every key it has signed with (the current one and each retired one from the key history: public halves only; the signing key never leaves `keys\`). The audit log records each download (`reader.download`) and each `gea reader` (`reader.write`) with the fingerprints the reader carried. The reader page writes nothing on the client's computer but the fingerprints a client saves (in the browser's storage for the page): it reads the zip it is given, in memory, and a saved copy goes where the client's browser saves downloads. The installed program also writes the page it opens to `%LOCALAPPDATA%\GEA-Reader\page` (with the dossier inside it when one was right-clicked; removed ten minutes later) and its right-click entry under the user's own registry keys, removed with the program.
10
10
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  > GEA Recorder is the program on the pad: a Raspberry Pi in a sealed box that records the ground channels (geophones, accelerometers, electrode dipoles) through a 24-bit ADC and the rig's WITS0 feed, every sample on one GPS-disciplined clock, into hourly miniSEED files with a SHA-256 beside each and a manifest, and serves one status page on the pad network. Nothing to type: power it and it records. GEA pulls its files in and makes stations of them.
4
4
 
5
- **The command**: the **Seismic** tab, the card **Field recorder**: the box's address on the pad network (`http://192.168.1.50:8780` - the status page shows the address) or the folder of the USB mirror carried to the office, a station name if the site's is not wanted, then **Pull** (a job). GEA fetches the manifest, downloads every file it does not have, checks each against its SHA-256, and makes or updates two stations: the array of the ground-motion sensors (band 8-80 Hz, the sensors' positions from the box's config) and the electrode station (0.01-5 Hz); each sensor's hours are joined in order into one record, nothing re-sampled or filtered. Pull again later: only the new hours come across, and the stations are updated in place. From a terminal: `gea workspace --path C:\site --action field-pull --box http://192.168.1.50:8780`. On the box: `gea-recorder status`, `gea-recorder selftest`; `sudo sh kiosk.sh` once puts the live view full-screen on a monitor on the box's HDMI port from boot - RECORDING in green or what is wrong in red, the clock, the rig's depth, every channel's last minute and the ground sensors' spectrogram, nothing to press - and the same view is at `/live` on the box's page from any browser on the pad network; `sudo sh install.sh` once, from the release's `GEA-Recorder-0.74.0.tar.gz`; the box's README has the parts list, the wiring and the three chrony lines for the GPS clock.
5
+ **The command**: the **Seismic** tab, the card **Field recorder**: the box's address on the pad network (`http://192.168.1.50:8780` - the status page shows the address) or the folder of the USB mirror carried to the office, a station name if the site's is not wanted, then **Pull** (a job). GEA fetches the manifest, downloads every file it does not have, checks each against its SHA-256, and makes or updates two stations: the array of the ground-motion sensors (band 8-80 Hz, the sensors' positions from the box's config) and the electrode station (0.01-5 Hz); each sensor's hours are joined in order into one record, nothing re-sampled or filtered. Pull again later: only the new hours come across, and the stations are updated in place. From a terminal: `gea workspace --path C:\site --action field-pull --box http://192.168.1.50:8780`. On the box: `gea-recorder status`, `gea-recorder selftest`; `sudo sh kiosk.sh` once puts the live view full-screen on a monitor on the box's HDMI port from boot - RECORDING in green or what is wrong in red, the clock, the rig's depth, every channel's last minute and the ground sensors' spectrogram, nothing to press - and the same view is at `/live` on the box's page from any browser on the pad network; `sudo sh install.sh` once, from the release's `GEA-Recorder-0.75.1.tar.gz`; the box's README has the parts list, the wiring and the three chrony lines for the GPS clock.
6
6
 
7
- **Before there is a Pi**: on the same card, **Start the simulated box**: the recorder's own program runs on this computer with its test signal in place of the ADC (a process of its own, its files under `field\sim-box\` in the workspace, its page on port 8781), **Open its monitor** shows the live screen the attendant will see, **Pull from it** brings its files in as stations, **Stop it** closes every file and lists them. **Open the box's monitor** opens any real box's live screen from its address. The kit and the setup install the recorder's program beside GEA for this; a pip install adds it with `pip install gea-recorder`.
7
+ **Before there is a Pi**: on the same card, **Start the simulated box**: the recorder's own program runs on this computer with its test signal in place of the ADC (a process of its own, its files under `field\sim-box\` in the workspace, its page on port 8781), **Open its monitor** shows the live screen the attendant will see, **Pull from it** brings its files in as stations, **Stop it** closes every file and lists them. **Open the box's monitor** opens any real box's live screen from its address. The kit and the setup install the recorder's program beside GEA for this; a pip install adds it with `pip install gea-recorder`. **From the office, over the internet**: GEA-VPN (`gea help vpn`, Administration) gives every box a private address the office pulls from (`http://10.77.0.3:8780`) through a tunnel from the pad's cellular modem; the box's zip from that card holds `vpn.sh`, run once on the Pi.
8
8
 
9
9
  **What it needs**: the box's kit on a Raspberry Pi 4 or 5 with the ADS1256 HAT (Waveshare High-Precision AD/DA), a GPS receiver with a pulse-per-second line on GPIO 18, buried geophones and electrode pairs wired to the ADC's inputs, and the box's config: the site, the wellhead's position, each channel's input, id, kind and place in feet from the wellhead, the ADC driver, the rig feed's port. The rig feed is optional and read-only; the EDR's WITS0 over a serial line or TCP, when the operator gives it.
10
10
 
@@ -0,0 +1,13 @@
1
+ # GEA-VPN - the private network between the programs
2
+
3
+ > The control panel, each pad's recorder, the operator's window and the client's reader run on separate computers in separate places; GEA-VPN joins them into one private network over the internet, so the office reaches every box at a fixed private address and nothing else on the internet can see any of them.
4
+
5
+ **The command**: the Administration tab's **GEA-VPN** card (admin). It is WireGuard - the VPN built into every Linux kernel and installed on Windows from wireguard.com - and GEA writes every key, address and configuration, so nothing is typed on a pad. One **hub** is needed: a small cloud server of the site's own, any Debian or Ubuntu machine with a public address (a rented virtual server of the smallest size is enough), reachable on UDP port 51820; every member connects out to it and it forwards between the office and each member - members never see one another. Make the network once: enter the hub's public name or address (`vpn.pad3site.com` or `203.0.113.10`) and click **Make the network** - the office member (this control panel, `10.77.0.2`) is made with it. Download `gea-vpn-hub.zip`, copy it to the hub and run there, once, `sudo sh install-hub.sh` (it installs WireGuard, opens the port, turns forwarding on and starts the tunnel at boot). Install WireGuard on this computer from wireguard.com (admin, once), click **Import tunnel(s) from file**, choose `gea-vpn.conf` from `gea-vpn-office.zip`, click **Activate**; the card's pill turns to `tunnel up`. Add a member per computer: its name (`Pad 3 box`) and its kind - `box` for a recorder (its page on 8780, reaches the office only), `operator` or `reader` for a window (reaches the office's control panel only), `office` for a second control panel - then **Add the member**; its address and `gea-vpn-pad-3-box.zip` appear in the table, and the hub installer must be run again (the hub's folder is written anew with every member). A box's zip holds `vpn.sh`: on the Pi, `sudo sh vpn.sh` once. A Windows member imports its `gea-vpn.conf` into WireGuard once. In the Field tab, pull from the box at its private address: `http://10.77.0.3:8780`. **Remove** takes a member out (its key is dropped from the hub when the installer runs again); **Make the files again** rewrites every folder and zip. From a terminal: `gea vpn --workspace C:\site --create vpn.pad3site.com`, `gea vpn --workspace C:\site --add "Pad 3 box" --kind box`, `gea vpn --workspace C:\site --remove pad-3-box`, `gea vpn --workspace C:\site --bundles`, `gea vpn --workspace C:\site` (the table).
6
+
7
+ **What it writes**: `keys\vpn\network.json` (owner-only; every member's private key, the hub's, and the pre-shared key per member) - the backup carries it, the dossier never does; under `exports\vpn\`: `hub\` (`gea-vpn.conf`, `install-hub.sh`, `README.txt`, zipped as `gea-vpn-hub.zip`) and one folder per member (`gea-vpn.conf` with the member's own private key, plus `vpn.sh` for a box or `README.txt` for a window, zipped as `gea-vpn-pad-3-box.zip` for `Pad 3 box`); the audit lines `vpn.create`, `vpn.hub`, `vpn.member.add`, `vpn.member.remove`, `vpn.bundles`, `vpn.bundle.download`. A member's zip holds its key: hand it over in person, or by a channel of its own, never by the same e-mail as anything else; a lost file is replaced by removing the member, adding it again and running the hub installer again.
8
+
9
+ **The number to check**: the card's pill - `tunnel up` means WireGuard on this computer has the `gea-vpn` tunnel active; `tunnel not up here` means it is not installed or not activated, and nothing private is reachable. In the table, **Last handshake** within the last two or three minutes for a member means its tunnel is up and talking to this computer (a keepalive every 25 s keeps it so behind a cellular modem); **Reachable** `yes` means the member's program answered on its port through the tunnel - a box's page on 8780, a window's control panel - and that is the address to pull from.
10
+
11
+ **What this page will not call a measurement**: a handshake is not a pull - a box reachable through the tunnel still has to be pulled in the Field tab for its files to become a station; the tunnel encrypts and addresses, it does not verify a file (the SHA-256 beside every record does that, through the tunnel or not); and the private addresses are the site's own convention - nothing of the network is known outside the hub and its members.
12
+
13
+ Related: `gea help recorder`, `gea help site`, `gea help backup`.
@@ -37,6 +37,7 @@ INDEX: List[tuple] = [
37
37
  ('sites', 'Sites - the engagement', 'the wells, stations and tracks of one client\'s ground, and the one report for all of it'),
38
38
  ('seismic', 'Seismic - the second leg', 'miniSEED/SAC in, spectra and persistent lines out, the detectability test against known rigs'),
39
39
  ('bit-image', 'The image - drill-bit seismic from the ground', "the drilling itself as the seismic source of known position: ground stations correlated against the one beside the rig, the velocity column layer by layer, the layer below the bit, the property column, and what a field recorder must capture - end to end on a labelled scene"),
40
+ ('vpn', 'GEA-VPN - the private network between the programs', "WireGuard managed from GEA: the hub on a small server of the site's own, a key and a configuration per computer, the office reaching each box at a fixed private address; nothing typed on a pad"),
40
41
  ('recorder', 'The field recorder - the black box', "GEA Recorder on a Raspberry Pi in the pad: the ground channels and the rig feed on one GPS clock into hourly miniSEED files with hashes and a manifest; GEA pulls them in and makes stations of them"),
41
42
  ('patches', 'Patches', 'supervised live connections, their states and records'),
42
43
  ('files', 'Files', 'import and export roots, detection by content, the evidence pack'),