gea-program 0.54.0__tar.gz → 0.55.0__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 (364) hide show
  1. {gea_program-0.54.0 → gea_program-0.55.0}/PKG-INFO +29 -5
  2. {gea_program-0.54.0 → gea_program-0.55.0}/README.md +28 -4
  3. {gea_program-0.54.0 → gea_program-0.55.0}/gea/__init__.py +1 -1
  4. {gea_program-0.54.0 → gea_program-0.55.0}/gea/__main__.py +62 -3
  5. {gea_program-0.54.0 → gea_program-0.55.0}/gea/acceptance_tests.py +223 -0
  6. {gea_program-0.54.0 → gea_program-0.55.0}/gea/alerts.py +15 -2
  7. {gea_program-0.54.0 → gea_program-0.55.0}/gea/doctor.py +12 -0
  8. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/alerts.md +1 -1
  9. gea_program-0.55.0/gea/help/https.md +19 -0
  10. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/upkeep.md +2 -2
  11. {gea_program-0.54.0 → gea_program-0.55.0}/gea/helplib.py +1 -0
  12. {gea_program-0.54.0 → gea_program-0.55.0}/gea/notify.py +3 -1
  13. {gea_program-0.54.0 → gea_program-0.55.0}/gea/service.py +149 -6
  14. gea_program-0.55.0/gea/tls.py +530 -0
  15. {gea_program-0.54.0 → gea_program-0.55.0}/gea/web/app.html +24 -8
  16. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/PKG-INFO +29 -5
  17. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/SOURCES.txt +2 -0
  18. {gea_program-0.54.0 → gea_program-0.55.0}/pyproject.toml +1 -1
  19. {gea_program-0.54.0 → gea_program-0.55.0}/LICENSE +0 -0
  20. {gea_program-0.54.0 → gea_program-0.55.0}/gea/BENCH_TEST_PROTOCOL.md +0 -0
  21. {gea_program-0.54.0 → gea_program-0.55.0}/gea/accuracy_statement.py +0 -0
  22. {gea_program-0.54.0 → gea_program-0.55.0}/gea/alarm_engine.py +0 -0
  23. {gea_program-0.54.0 → gea_program-0.55.0}/gea/backup.py +0 -0
  24. {gea_program-0.54.0 → gea_program-0.55.0}/gea/bench.py +0 -0
  25. {gea_program-0.54.0 → gea_program-0.55.0}/gea/blind_harness.py +0 -0
  26. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/acex_lomonosov_age_depth_model.provenance.json +0 -0
  27. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/acex_lomonosov_age_depth_model.txt +0 -0
  28. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/agassiz77_canada_temperature.csv +0 -0
  29. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/agassiz77_canada_temperature.provenance.json +0 -0
  30. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/barbados_110_consolidation.provenance.json +0 -0
  31. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/barbados_110_consolidation.txt +0 -0
  32. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/bengal_u1452_grain_size.provenance.json +0 -0
  33. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/bengal_u1452_grain_size.txt +0 -0
  34. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/blake_164_methane_isotopes.provenance.json +0 -0
  35. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/blake_164_methane_isotopes.txt +0 -0
  36. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +0 -0
  37. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/chicxulub_m0077a_pwave_velocity.txt +0 -0
  38. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/collingwood_1_28_ks_complete.las +0 -0
  39. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/collingwood_1_28_ks_complete.provenance.json +0 -0
  40. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/costa_rica_odp_friction_envelope.provenance.json +0 -0
  41. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/costa_rica_odp_friction_envelope.txt +0 -0
  42. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +0 -0
  43. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +0 -0
  44. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dsdp_504b_physical_properties.provenance.json +0 -0
  45. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dsdp_504b_physical_properties.txt +0 -0
  46. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dsdp_504b_sound_velocity.provenance.json +0 -0
  47. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/dsdp_504b_sound_velocity.txt +0 -0
  48. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/elgygytgyn_5011_turbidites.provenance.json +0 -0
  49. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/elgygytgyn_5011_turbidites.txt +0 -0
  50. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/epica_domec_co2_800kyr.provenance.json +0 -0
  51. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/epica_domec_co2_800kyr.txt +0 -0
  52. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/fram_909_organic_petrography.provenance.json +0 -0
  53. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/fram_909_organic_petrography.txt +0 -0
  54. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gbr_325_coral_uth_ages.provenance.json +0 -0
  55. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gbr_325_coral_uth_ages.txt +0 -0
  56. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gisp2_greenland_temperature.csv +0 -0
  57. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gisp2_greenland_temperature.provenance.json +0 -0
  58. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gom_308_t2p_insitu.provenance.json +0 -0
  59. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/gom_308_t2p_insitu.txt +0 -0
  60. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/guaymas_385_dom_d13c.provenance.json +0 -0
  61. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/guaymas_385_dom_d13c.txt +0 -0
  62. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +0 -0
  63. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/hikurangi_u1520_friction_insitu.txt +0 -0
  64. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +0 -0
  65. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +0 -0
  66. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/iodp_u1324_pore_pressure.provenance.json +0 -0
  67. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/iodp_u1324_pore_pressure.txt +0 -0
  68. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/jfast_c0019_slow_slip_events.provenance.json +0 -0
  69. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/jfast_c0019_slow_slip_events.txt +0 -0
  70. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/kennetcook_2_p129_excerpt.las +0 -0
  71. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/kennetcook_2_p129_excerpt.provenance.json +0 -0
  72. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_bhgm_density.dat +0 -0
  73. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_bhgm_density.provenance.json +0 -0
  74. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +0 -0
  75. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_complog_6020_excerpt.txt +0 -0
  76. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_hlog246_temperature.dat +0 -0
  77. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_hlog246_temperature.provenance.json +0 -0
  78. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_rockmech_compress.dat +0 -0
  79. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_rockmech_compress.provenance.json +0 -0
  80. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +0 -0
  81. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +0 -0
  82. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_vb_rockmech_compress.dat +0 -0
  83. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_vb_rockmech_compress.provenance.json +0 -0
  84. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_vb_vlog251_temperature.dat +0 -0
  85. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ktb_vb_vlog251_temperature.provenance.json +0 -0
  86. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/l06_06_nl_survey.csv +0 -0
  87. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/l06_06_nl_survey.provenance.json +0 -0
  88. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/l07_01_nl_excerpt.las +0 -0
  89. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/l07_01_nl_excerpt.provenance.json +0 -0
  90. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +0 -0
  91. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/mariana_1200_serpentinite_geochem.txt +0 -0
  92. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/med_160_sapropels.provenance.json +0 -0
  93. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/med_160_sapropels.txt +0 -0
  94. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/nankai_megasplay_shear_strength.provenance.json +0 -0
  95. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/nankai_megasplay_shear_strength.txt +0 -0
  96. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1027b_thermal_conductivity.provenance.json +0 -0
  97. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1027b_thermal_conductivity.txt +0 -0
  98. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1027c_cork_temperature.provenance.json +0 -0
  99. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1027c_cork_temperature.txt +0 -0
  100. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1165b_thermal_conductivity.provenance.json +0 -0
  101. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1165b_thermal_conductivity.txt +0 -0
  102. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +0 -0
  103. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_1274a_mantle_peridotite_mad.txt +0 -0
  104. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +0 -0
  105. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_504b_dike_elastic_moduli.txt +0 -0
  106. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +0 -0
  107. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_504b_leg137_borehole_fluids.txt +0 -0
  108. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +0 -0
  109. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/odp_735b_gabbro_elastic_moduli.txt +0 -0
  110. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/peru_201_sulfate_reduction.provenance.json +0 -0
  111. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/peru_201_sulfate_reduction.txt +0 -0
  112. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/scorpio_e1_sa_excerpt.las +0 -0
  113. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/scorpio_e1_sa_excerpt.provenance.json +0 -0
  114. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/sumatra_362_cohesion.provenance.json +0 -0
  115. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/sumatra_362_cohesion.txt +0 -0
  116. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/university_6_17_no1_tx_excerpt.las +0 -0
  117. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +0 -0
  118. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ursa_308_xrd_mineralogy.provenance.json +0 -0
  119. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/ursa_308_xrd_mineralogy.txt +0 -0
  120. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_15_9_19_sr_excerpt.las +0 -0
  121. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +0 -0
  122. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_15_9_19a_core_excerpt.csv +0 -0
  123. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +0 -0
  124. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_f12_f14_production_excerpt.csv +0 -0
  125. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/volve_f12_f14_production_excerpt.provenance.json +0 -0
  126. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/walvis_208_petm_carbonate.provenance.json +0 -0
  127. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/walvis_208_petm_carbonate.txt +0 -0
  128. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/woodlark_1109_rock_eval.provenance.json +0 -0
  129. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog/woodlark_1109_rock_eval.txt +0 -0
  130. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog_map.py +0 -0
  131. {gea_program-0.54.0 → gea_program-0.55.0}/gea/catalog_register.py +0 -0
  132. {gea_program-0.54.0 → gea_program-0.55.0}/gea/certificates.py +0 -0
  133. {gea_program-0.54.0 → gea_program-0.55.0}/gea/cli.py +0 -0
  134. {gea_program-0.54.0 → gea_program-0.55.0}/gea/client_reports.py +0 -0
  135. {gea_program-0.54.0 → gea_program-0.55.0}/gea/collision.py +0 -0
  136. {gea_program-0.54.0 → gea_program-0.55.0}/gea/config_versioning.py +0 -0
  137. {gea_program-0.54.0 → gea_program-0.55.0}/gea/conformance.py +0 -0
  138. {gea_program-0.54.0 → gea_program-0.55.0}/gea/control_room.py +0 -0
  139. {gea_program-0.54.0 → gea_program-0.55.0}/gea/correlation.py +0 -0
  140. {gea_program-0.54.0 → gea_program-0.55.0}/gea/dashboard.py +0 -0
  141. {gea_program-0.54.0 → gea_program-0.55.0}/gea/declarations.py +0 -0
  142. {gea_program-0.54.0 → gea_program-0.55.0}/gea/decline.py +0 -0
  143. {gea_program-0.54.0 → gea_program-0.55.0}/gea/deviation.py +0 -0
  144. {gea_program-0.54.0 → gea_program-0.55.0}/gea/dlis.py +0 -0
  145. {gea_program-0.54.0 → gea_program-0.55.0}/gea/dnp3.py +0 -0
  146. {gea_program-0.54.0 → gea_program-0.55.0}/gea/dossier.py +0 -0
  147. {gea_program-0.54.0 → gea_program-0.55.0}/gea/dossier_verify.py +0 -0
  148. {gea_program-0.54.0 → gea_program-0.55.0}/gea/downhole_engine.py +0 -0
  149. {gea_program-0.54.0 → gea_program-0.55.0}/gea/drift_monitor.py +0 -0
  150. {gea_program-0.54.0 → gea_program-0.55.0}/gea/drill_string.py +0 -0
  151. {gea_program-0.54.0 → gea_program-0.55.0}/gea/earth_model.py +0 -0
  152. {gea_program-0.54.0 → gea_program-0.55.0}/gea/economics.py +0 -0
  153. {gea_program-0.54.0 → gea_program-0.55.0}/gea/ed25519.py +0 -0
  154. {gea_program-0.54.0 → gea_program-0.55.0}/gea/edr.py +0 -0
  155. {gea_program-0.54.0 → gea_program-0.55.0}/gea/elevation.py +0 -0
  156. {gea_program-0.54.0 → gea_program-0.55.0}/gea/etp.py +0 -0
  157. {gea_program-0.54.0 → gea_program-0.55.0}/gea/etp12_schemas.json +0 -0
  158. {gea_program-0.54.0 → gea_program-0.55.0}/gea/example_register_map.json +0 -0
  159. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fat_sat.py +0 -0
  160. {gea_program-0.54.0 → gea_program-0.55.0}/gea/files.py +0 -0
  161. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fixtures/README.md +0 -0
  162. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fixtures/comcat_delaware_basin_2026-09.txt +0 -0
  163. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fixtures/comcat_mahoning_1990-2026.txt +0 -0
  164. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fixtures/comcat_youngstown_2011-2012.txt +0 -0
  165. {gea_program-0.54.0 → gea_program-0.55.0}/gea/fixtures/ne_110m_coastline.json +0 -0
  166. {gea_program-0.54.0 → gea_program-0.55.0}/gea/follower.py +0 -0
  167. {gea_program-0.54.0 → gea_program-0.55.0}/gea/forward_model.py +0 -0
  168. {gea_program-0.54.0 → gea_program-0.55.0}/gea/frac_hits.py +0 -0
  169. {gea_program-0.54.0 → gea_program-0.55.0}/gea/friction_priors.py +0 -0
  170. {gea_program-0.54.0 → gea_program-0.55.0}/gea/gamma.py +0 -0
  171. {gea_program-0.54.0 → gea_program-0.55.0}/gea/gauge_aging.py +0 -0
  172. {gea_program-0.54.0 → gea_program-0.55.0}/gea/gauge_specs.py +0 -0
  173. {gea_program-0.54.0 → gea_program-0.55.0}/gea/geodesy.py +0 -0
  174. {gea_program-0.54.0 → gea_program-0.55.0}/gea/geomag.py +0 -0
  175. {gea_program-0.54.0 → gea_program-0.55.0}/gea/gravity_reference.py +0 -0
  176. {gea_program-0.54.0 → gea_program-0.55.0}/gea/hartip.py +0 -0
  177. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/TESTER_GUIDE.md +0 -0
  178. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/alarms.md +0 -0
  179. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/audit-update.md +0 -0
  180. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/backup.md +0 -0
  181. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/catalog.md +0 -0
  182. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/charts.md +0 -0
  183. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/collision.md +0 -0
  184. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/conformance.md +0 -0
  185. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/control-room.md +0 -0
  186. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/data-in.md +0 -0
  187. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/declare.md +0 -0
  188. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/decline.md +0 -0
  189. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/dlis.md +0 -0
  190. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/doctor.md +0 -0
  191. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/dossier.md +0 -0
  192. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/drift.md +0 -0
  193. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/drillstring.md +0 -0
  194. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/economics.md +0 -0
  195. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/edr.md +0 -0
  196. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/etp.md +0 -0
  197. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/field-ports.md +0 -0
  198. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/files.md +0 -0
  199. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/frac-hits.md +0 -0
  200. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/historian.md +0 -0
  201. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/instruments.md +0 -0
  202. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/interval.md +0 -0
  203. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/invoice-check.md +0 -0
  204. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/map.md +0 -0
  205. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/month-end.md +0 -0
  206. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/notifications.md +0 -0
  207. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/opseis.md +0 -0
  208. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/osdu.md +0 -0
  209. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/pad-summary.md +0 -0
  210. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/patches.md +0 -0
  211. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/penetrator.md +0 -0
  212. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/plan.md +0 -0
  213. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/pore-pressure.md +0 -0
  214. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/ppdm.md +0 -0
  215. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/quakeml.md +0 -0
  216. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/quality.md +0 -0
  217. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/report-history.md +0 -0
  218. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/rig-time.md +0 -0
  219. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/seedlink.md +0 -0
  220. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/seismic.md +0 -0
  221. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/setup.md +0 -0
  222. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/simulation.md +0 -0
  223. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/site.md +0 -0
  224. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/sites.md +0 -0
  225. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/slip.md +0 -0
  226. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/soak.md +0 -0
  227. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/sra.md +0 -0
  228. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/start.md +0 -0
  229. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/structure.md +0 -0
  230. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/tools.md +0 -0
  231. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/torque-drag.md +0 -0
  232. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/transients.md +0 -0
  233. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/vibration.md +0 -0
  234. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/well-report.md +0 -0
  235. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/well-tests.md +0 -0
  236. {gea_program-0.54.0 → gea_program-0.55.0}/gea/help/well3d.md +0 -0
  237. {gea_program-0.54.0 → gea_program-0.55.0}/gea/housekeeping.py +0 -0
  238. {gea_program-0.54.0 → gea_program-0.55.0}/gea/interval.py +0 -0
  239. {gea_program-0.54.0 → gea_program-0.55.0}/gea/inverse_engine.py +0 -0
  240. {gea_program-0.54.0 → gea_program-0.55.0}/gea/invoice_check.py +0 -0
  241. {gea_program-0.54.0 → gea_program-0.55.0}/gea/jobs.py +0 -0
  242. {gea_program-0.54.0 → gea_program-0.55.0}/gea/live_ports.py +0 -0
  243. {gea_program-0.54.0 → gea_program-0.55.0}/gea/loadtest.py +0 -0
  244. {gea_program-0.54.0 → gea_program-0.55.0}/gea/matplotlib_demo.py +0 -0
  245. {gea_program-0.54.0 → gea_program-0.55.0}/gea/modbus.py +0 -0
  246. {gea_program-0.54.0 → gea_program-0.55.0}/gea/modbus_rtu.py +0 -0
  247. {gea_program-0.54.0 → gea_program-0.55.0}/gea/model_card.py +0 -0
  248. {gea_program-0.54.0 → gea_program-0.55.0}/gea/month_end.py +0 -0
  249. {gea_program-0.54.0 → gea_program-0.55.0}/gea/mqtt_port.py +0 -0
  250. {gea_program-0.54.0 → gea_program-0.55.0}/gea/op_seismicity.py +0 -0
  251. {gea_program-0.54.0 → gea_program-0.55.0}/gea/opcua_port.py +0 -0
  252. {gea_program-0.54.0 → gea_program-0.55.0}/gea/operator_app.py +0 -0
  253. {gea_program-0.54.0 → gea_program-0.55.0}/gea/osdu.py +0 -0
  254. {gea_program-0.54.0 → gea_program-0.55.0}/gea/pad_summary.py +0 -0
  255. {gea_program-0.54.0 → gea_program-0.55.0}/gea/patches.py +0 -0
  256. {gea_program-0.54.0 → gea_program-0.55.0}/gea/pdf_report.py +0 -0
  257. {gea_program-0.54.0 → gea_program-0.55.0}/gea/penetrator.py +0 -0
  258. {gea_program-0.54.0 → gea_program-0.55.0}/gea/permits.py +0 -0
  259. {gea_program-0.54.0 → gea_program-0.55.0}/gea/piwebapi.py +0 -0
  260. {gea_program-0.54.0 → gea_program-0.55.0}/gea/pore_pressure.py +0 -0
  261. {gea_program-0.54.0 → gea_program-0.55.0}/gea/ports.py +0 -0
  262. {gea_program-0.54.0 → gea_program-0.55.0}/gea/ppdm.py +0 -0
  263. {gea_program-0.54.0 → gea_program-0.55.0}/gea/profile_catalog.py +0 -0
  264. {gea_program-0.54.0 → gea_program-0.55.0}/gea/project.py +0 -0
  265. {gea_program-0.54.0 → gea_program-0.55.0}/gea/qt6_downhole_app.py +0 -0
  266. {gea_program-0.54.0 → gea_program-0.55.0}/gea/quakeml.py +0 -0
  267. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reconciler.py +0 -0
  268. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/IU_ANMO_10_BHZ_response.provenance.json +0 -0
  269. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/IU_ANMO_10_BHZ_response.xml +0 -0
  270. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/WMM2025.COF +0 -0
  271. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/eia_henry_hub_daily.csv.gz +0 -0
  272. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/eia_prices.provenance.json +0 -0
  273. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/eia_wti_daily.csv.gz +0 -0
  274. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/iscwsa_rev4_example.json +0 -0
  275. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/iscwsa_rev5_example.json +0 -0
  276. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/l0701_sonic_gr.las.gz +0 -0
  277. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/l0701_sonic_gr.provenance.json +0 -0
  278. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/libmseed_steim.provenance.json +0 -0
  279. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/libmseed_steim1_4096.mseed +0 -0
  280. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/libmseed_steim2_4096.mseed +0 -0
  281. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/libmseed_steim2_512.mseed +0 -0
  282. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/mahoning_example.json +0 -0
  283. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/mud_motor_specs.json +0 -0
  284. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/owsg_tool_models.json +0 -0
  285. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/penetrator_ratings.json +0 -0
  286. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/seismicity_protocols.json +0 -0
  287. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/tool_sources.json +0 -0
  288. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/us_outline.json +0 -0
  289. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/us_outline.provenance.json +0 -0
  290. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_3d.provenance.json +0 -0
  291. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_f4_interpretation.las.gz +0 -0
  292. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_f4_logs.provenance.json +0 -0
  293. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_f4_lwd.las.gz +0 -0
  294. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_surfaces.json.gz +0 -0
  295. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-10.xml.gz +0 -0
  296. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-4.xml.gz +0 -0
  297. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-5_12.25in.xml.gz +0 -0
  298. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-5_17.5in.xml.gz +0 -0
  299. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-5_36in.xml.gz +0 -0
  300. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_traj_F-5_8.5in.xml.gz +0 -0
  301. {gea_program-0.54.0 → gea_program-0.55.0}/gea/reference/volve_well_picks.csv.gz +0 -0
  302. {gea_program-0.54.0 → gea_program-0.55.0}/gea/report_history.py +0 -0
  303. {gea_program-0.54.0 → gea_program-0.55.0}/gea/rig_state.py +0 -0
  304. {gea_program-0.54.0 → gea_program-0.55.0}/gea/rock_inventory.py +0 -0
  305. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sample_record.py +0 -0
  306. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sample_well_profile.csv +0 -0
  307. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sbom.py +0 -0
  308. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seedlink.py +0 -0
  309. {gea_program-0.54.0 → gea_program-0.55.0}/gea/segy.py +0 -0
  310. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic.py +0 -0
  311. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_array.py +0 -0
  312. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_assoc.py +0 -0
  313. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_detect.py +0 -0
  314. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_film.py +0 -0
  315. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_harmonic.py +0 -0
  316. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_qc.py +0 -0
  317. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_response.py +0 -0
  318. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_signature.py +0 -0
  319. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_track.py +0 -0
  320. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_uncertainty.py +0 -0
  321. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_unlisted.py +0 -0
  322. {gea_program-0.54.0 → gea_program-0.55.0}/gea/seismic_watch.py +0 -0
  323. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sensor_swap.py +0 -0
  324. {gea_program-0.54.0 → gea_program-0.55.0}/gea/series_cache.py +0 -0
  325. {gea_program-0.54.0 → gea_program-0.55.0}/gea/service_life.py +0 -0
  326. {gea_program-0.54.0 → gea_program-0.55.0}/gea/setup_checklist.py +0 -0
  327. {gea_program-0.54.0 → gea_program-0.55.0}/gea/shapefile.py +0 -0
  328. {gea_program-0.54.0 → gea_program-0.55.0}/gea/shell.py +0 -0
  329. {gea_program-0.54.0 → gea_program-0.55.0}/gea/shut_in.py +0 -0
  330. {gea_program-0.54.0 → gea_program-0.55.0}/gea/simulation.py +0 -0
  331. {gea_program-0.54.0 → gea_program-0.55.0}/gea/site_map.py +0 -0
  332. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sla_report.py +0 -0
  333. {gea_program-0.54.0 → gea_program-0.55.0}/gea/slip.py +0 -0
  334. {gea_program-0.54.0 → gea_program-0.55.0}/gea/soak.py +0 -0
  335. {gea_program-0.54.0 → gea_program-0.55.0}/gea/sra.py +0 -0
  336. {gea_program-0.54.0 → gea_program-0.55.0}/gea/store_forward.py +0 -0
  337. {gea_program-0.54.0 → gea_program-0.55.0}/gea/strata_join.py +0 -0
  338. {gea_program-0.54.0 → gea_program-0.55.0}/gea/structure.py +0 -0
  339. {gea_program-0.54.0 → gea_program-0.55.0}/gea/supervisor.py +0 -0
  340. {gea_program-0.54.0 → gea_program-0.55.0}/gea/survey_cmd.py +0 -0
  341. {gea_program-0.54.0 → gea_program-0.55.0}/gea/survey_view.py +0 -0
  342. {gea_program-0.54.0 → gea_program-0.55.0}/gea/telemetry.py +0 -0
  343. {gea_program-0.54.0 → gea_program-0.55.0}/gea/tool_catalog.py +0 -0
  344. {gea_program-0.54.0 → gea_program-0.55.0}/gea/tool_library.py +0 -0
  345. {gea_program-0.54.0 → gea_program-0.55.0}/gea/tool_taxonomy.py +0 -0
  346. {gea_program-0.54.0 → gea_program-0.55.0}/gea/torque_drag.py +0 -0
  347. {gea_program-0.54.0 → gea_program-0.55.0}/gea/transient.py +0 -0
  348. {gea_program-0.54.0 → gea_program-0.55.0}/gea/vibration.py +0 -0
  349. {gea_program-0.54.0 → gea_program-0.55.0}/gea/well3d.py +0 -0
  350. {gea_program-0.54.0 → gea_program-0.55.0}/gea/well_assembler.py +0 -0
  351. {gea_program-0.54.0 → gea_program-0.55.0}/gea/well_report.py +0 -0
  352. {gea_program-0.54.0 → gea_program-0.55.0}/gea/well_test_validation.py +0 -0
  353. {gea_program-0.54.0 → gea_program-0.55.0}/gea/wellplan.py +0 -0
  354. {gea_program-0.54.0 → gea_program-0.55.0}/gea/wellpos.py +0 -0
  355. {gea_program-0.54.0 → gea_program-0.55.0}/gea/wits0.py +0 -0
  356. {gea_program-0.54.0 → gea_program-0.55.0}/gea/witsml.py +0 -0
  357. {gea_program-0.54.0 → gea_program-0.55.0}/gea/workspace.py +0 -0
  358. {gea_program-0.54.0 → gea_program-0.55.0}/gea/xlsx_read.py +0 -0
  359. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/dependency_links.txt +0 -0
  360. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/entry_points.txt +0 -0
  361. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/requires.txt +0 -0
  362. {gea_program-0.54.0 → gea_program-0.55.0}/gea_program.egg-info/top_level.txt +0 -0
  363. {gea_program-0.54.0 → gea_program-0.55.0}/setup.cfg +0 -0
  364. {gea_program-0.54.0 → gea_program-0.55.0}/tests/test_acceptance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gea-program
3
- Version: 0.54.0
3
+ Version: 0.55.0
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
@@ -243,7 +243,7 @@ disappearing. Every action is written to `records/audit.jsonl` with the
243
243
  actor's name and the SHA-256 of the inputs it used. The service is written on
244
244
  the standard library only (no new dependency, the standalone check still
245
245
  passes), listens on the loopback interface by default (`--host 0.0.0.0` for a
246
- site network, behind the site's TLS proxy), keeps sessions in HttpOnly
246
+ site network, over HTTPS - its own, below, or the site's TLS proxy), keeps sessions in HttpOnly
247
247
  SameSite cookies, and requires the page's own header on every action so a
248
248
  foreign form cannot act. Accounts live in `users.json` as salted PBKDF2-SHA256
249
249
  hashes; `gea users --workspace C:\site --action add --name ... --role admin`
@@ -1249,11 +1249,35 @@ is recomputed in the index. On the Reports page: the packets on record,
1249
1249
  Assemble as a job, and Schedule monthly (day and hour, the previous month).
1250
1250
  `gea help month-end`.
1251
1251
 
1252
+ ### HTTPS
1253
+
1254
+ The service speaks TLS itself when the workspace names a certificate: the
1255
+ site's own (from the company's certificate authority, which managed
1256
+ computers already trust; read where the site keeps it, at every start) or a
1257
+ self-signed one it makes with the standard library alone (an RSA key and an
1258
+ X.509 v3 server certificate for every name and address it is reached by).
1259
+ TLS 1.2 is the floor; the cookie is marked Secure; every answer carries
1260
+ Strict-Transport-Security; a plain `http://` request on the same port is
1261
+ sent to HTTPS; a certificate in its last 30 days raises the `tls.expiring`
1262
+ alert; a workspace whose certificate files are broken does not start rather
1263
+ than fall back to plain HTTP.
1264
+
1265
+ ```
1266
+ gea tls --workspace C:\site --site-cert C:\certs\gea-pad3.crt --site-key C:\certs\gea-pad3.key
1267
+ gea tls --workspace C:\site --self-signed --name gea-pad3 --name 10.20.0.15 --export-cert C:\certs\gea-site.crt
1268
+ gea serve --workspace C:\site --host 0.0.0.0 # https://gea-pad3:8765/
1269
+ ```
1270
+
1271
+ Or Administration -> **HTTPS** in the page (download the certificate, not
1272
+ the key, for the site's computers to trust; compare the SHA-256 shown).
1273
+ `gea help https`.
1274
+
1252
1275
  ### Before a site goes live
1253
1276
 
1254
- `deploy/README.md` is the checklist: keep the service on the loopback
1255
- address and put a TLS proxy in front (`deploy/Caddyfile`,
1256
- `deploy/nginx-gea.conf`), start with `--behind-proxy`. The service itself
1277
+ `deploy/README.md` is the checklist: HTTPS from the service itself (above),
1278
+ or keep the service on the loopback address and put the site's TLS proxy in
1279
+ front (`deploy/Caddyfile`, `deploy/nginx-gea.conf`), start with
1280
+ `--behind-proxy`. The service itself
1257
1281
  rate-limits sign-ins (five failures lock the name and the address for 15
1258
1282
  minutes, audited), lists and revokes live sessions, sends the security
1259
1283
  headers on every response, and `gea housekeeping --workspace C:\site --apply`
@@ -204,7 +204,7 @@ disappearing. Every action is written to `records/audit.jsonl` with the
204
204
  actor's name and the SHA-256 of the inputs it used. The service is written on
205
205
  the standard library only (no new dependency, the standalone check still
206
206
  passes), listens on the loopback interface by default (`--host 0.0.0.0` for a
207
- site network, behind the site's TLS proxy), keeps sessions in HttpOnly
207
+ site network, over HTTPS - its own, below, or the site's TLS proxy), keeps sessions in HttpOnly
208
208
  SameSite cookies, and requires the page's own header on every action so a
209
209
  foreign form cannot act. Accounts live in `users.json` as salted PBKDF2-SHA256
210
210
  hashes; `gea users --workspace C:\site --action add --name ... --role admin`
@@ -1210,11 +1210,35 @@ is recomputed in the index. On the Reports page: the packets on record,
1210
1210
  Assemble as a job, and Schedule monthly (day and hour, the previous month).
1211
1211
  `gea help month-end`.
1212
1212
 
1213
+ ### HTTPS
1214
+
1215
+ The service speaks TLS itself when the workspace names a certificate: the
1216
+ site's own (from the company's certificate authority, which managed
1217
+ computers already trust; read where the site keeps it, at every start) or a
1218
+ self-signed one it makes with the standard library alone (an RSA key and an
1219
+ X.509 v3 server certificate for every name and address it is reached by).
1220
+ TLS 1.2 is the floor; the cookie is marked Secure; every answer carries
1221
+ Strict-Transport-Security; a plain `http://` request on the same port is
1222
+ sent to HTTPS; a certificate in its last 30 days raises the `tls.expiring`
1223
+ alert; a workspace whose certificate files are broken does not start rather
1224
+ than fall back to plain HTTP.
1225
+
1226
+ ```
1227
+ gea tls --workspace C:\site --site-cert C:\certs\gea-pad3.crt --site-key C:\certs\gea-pad3.key
1228
+ gea tls --workspace C:\site --self-signed --name gea-pad3 --name 10.20.0.15 --export-cert C:\certs\gea-site.crt
1229
+ gea serve --workspace C:\site --host 0.0.0.0 # https://gea-pad3:8765/
1230
+ ```
1231
+
1232
+ Or Administration -> **HTTPS** in the page (download the certificate, not
1233
+ the key, for the site's computers to trust; compare the SHA-256 shown).
1234
+ `gea help https`.
1235
+
1213
1236
  ### Before a site goes live
1214
1237
 
1215
- `deploy/README.md` is the checklist: keep the service on the loopback
1216
- address and put a TLS proxy in front (`deploy/Caddyfile`,
1217
- `deploy/nginx-gea.conf`), start with `--behind-proxy`. The service itself
1238
+ `deploy/README.md` is the checklist: HTTPS from the service itself (above),
1239
+ or keep the service on the loopback address and put the site's TLS proxy in
1240
+ front (`deploy/Caddyfile`, `deploy/nginx-gea.conf`), start with
1241
+ `--behind-proxy`. The service itself
1218
1242
  rate-limits sign-ins (five failures lock the name and the address for 15
1219
1243
  minutes, audited), lists and revokes live sessions, sends the security
1220
1244
  headers on every response, and `gea housekeeping --workspace C:\site --apply`
@@ -123,7 +123,7 @@ from .profile_catalog import (
123
123
  read_pangaea_txt,
124
124
  )
125
125
 
126
- __version__ = "0.54.0"
126
+ __version__ = "0.55.0"
127
127
  __all__ = [
128
128
  "aging_rate", "rate_psi_yr", "accuracy_psi",
129
129
  "Sensor", "SimulatorConfig", "DownholeEngine",
@@ -756,13 +756,26 @@ def main(argv=None) -> int:
756
756
 
757
757
  p_sv = sub.add_parser("serve", help="serve the dashboard over a workspace: the page, the reports and the JSON API (standard library HTTP)")
758
758
  p_sv.add_argument("--workspace", type=str, required=True, help="the workspace folder (gea workspace --action init)")
759
- p_sv.add_argument("--host", type=str, default="127.0.0.1", help="bind address; 0.0.0.0 exposes it on the site network (put TLS in front)")
759
+ p_sv.add_argument("--host", type=str, default="127.0.0.1", help="bind address; 0.0.0.0 exposes it on the site network (turn HTTPS on first: gea tls)")
760
760
  p_sv.add_argument("--port", type=int, default=8765)
761
761
  p_sv.add_argument("--workers", type=int, default=1, help="jobs run at once")
762
762
  p_sv.add_argument("--no-scheduler", action="store_true", help="do not run scheduled jobs from this process")
763
763
  p_sv.add_argument("--behind-proxy", action="store_true", help="a reverse proxy terminates TLS in front: trust X-Forwarded-For/-Proto, mark the cookie Secure (deploy/ has nginx and Caddy examples)")
764
764
  p_sv.add_argument("--no-browser", action="store_true", help="do not open the control panel in a browser once the service is listening (a scheduled start, a restart, a server with no desktop)")
765
765
 
766
+ p_tl = sub.add_parser("tls", help="HTTPS for the dashboard: the site's own certificate, a self-signed one made here, or off; with no option, how it stands")
767
+ p_tl.add_argument("--workspace", type=str, required=True)
768
+ p_tl.add_argument("--site-cert", type=str, default=None, help="the site's certificate (PEM, the server's first, then any chain) - read from where it is at every start")
769
+ p_tl.add_argument("--site-key", type=str, default=None, help="its private key (PEM, without a passphrase)")
770
+ p_tl.add_argument("--self-signed", action="store_true", help="make a self-signed certificate and key in the workspace's keys\\tls folder")
771
+ p_tl.add_argument("--name", action="append", default=[], help="--self-signed: a host name or address the dashboard is reached by (repeat; default: this machine's names, localhost, 127.0.0.1)")
772
+ p_tl.add_argument("--days", type=int, default=825, help="--self-signed: days valid (at most 825)")
773
+ p_tl.add_argument("--bits", type=int, default=2048, choices=[2048, 3072])
774
+ p_tl.add_argument("--off", action="store_true", help="plain HTTP from the next start (the files are left where they are)")
775
+ p_tl.add_argument("--export-cert", type=str, default=None, help="write the certificate (public, never the key) to this file, for the site's computers to trust")
776
+ p_tl.add_argument("--actor", type=str, default="cli")
777
+ p_tl.add_argument("--json", action="store_true")
778
+
766
779
  p_hk = sub.add_parser("housekeeping", help="rotate the append-only logs, prune finished job folders and old live recordings (dry run unless --apply)")
767
780
  p_hk.add_argument("--workspace", type=str, required=True)
768
781
  p_hk.add_argument("--apply", action="store_true")
@@ -2603,6 +2616,41 @@ def main(argv=None) -> int:
2603
2616
  raise SystemExit(f"pore-pressure: {e.args[0] if e.args else e}")
2604
2617
  print(_json.dumps(r, default=str) if a.json else _PPF.text(r))
2605
2618
  return 0
2619
+ elif a.cmd == "tls":
2620
+ import json as _json
2621
+ from . import tls as _TLS
2622
+ from .workspace import Workspace, WorkspaceError
2623
+ try:
2624
+ ws = Workspace(a.workspace)
2625
+ if sum(bool(x) for x in (a.self_signed, a.site_cert or a.site_key, a.off)) > 1:
2626
+ raise ValueError("one of --self-signed, --site-cert/--site-key or --off")
2627
+ if a.site_cert or a.site_key:
2628
+ if not (a.site_cert and a.site_key):
2629
+ raise ValueError("--site-cert and --site-key go together")
2630
+ r = _TLS.use_site_certificate(ws, a.actor, a.site_cert, a.site_key)
2631
+ elif a.self_signed:
2632
+ r = _TLS.make_self_signed(ws, a.actor, a.name or None, a.days, a.bits)
2633
+ elif a.off:
2634
+ r = _TLS.turn_off(ws, a.actor)
2635
+ else:
2636
+ r = _TLS.status(ws)
2637
+ if a.export_cert:
2638
+ pem = _TLS.certificate_pem(ws)
2639
+ if not pem:
2640
+ raise ValueError("HTTPS is off: there is no certificate to export")
2641
+ with open(a.export_cert, "wb") as f_:
2642
+ f_.write(pem)
2643
+ except (KeyError, ValueError, OSError, WorkspaceError) as e:
2644
+ raise SystemExit(f"tls: {e.args[0] if e.args else e}")
2645
+ if a.json:
2646
+ print(_json.dumps(r, default=str))
2647
+ else:
2648
+ print(_TLS.text(r))
2649
+ if a.self_signed or a.site_cert or a.off:
2650
+ print(" takes effect at the next start of the control panel (gea serve, or Restart on the Administration page)")
2651
+ if a.export_cert:
2652
+ print(f" certificate written to {a.export_cert} - hand this out, never the key")
2653
+ return 0
2606
2654
  elif a.cmd == "frac-hits":
2607
2655
  import json as _json
2608
2656
  from . import frac_hits as _FH
@@ -2946,18 +2994,29 @@ def main(argv=None) -> int:
2946
2994
  print(f"serve: BLOCK {mism}")
2947
2995
  if pre or mism:
2948
2996
  raise SystemExit("serve: not starting - fix the above (gea doctor --workspace ... shows the full picture)")
2997
+ from .tls import TlsError
2949
2998
  try:
2950
2999
  svc = Service(a.workspace, host=a.host, port=a.port, workers=a.workers, scheduler=not a.no_scheduler, behind_proxy=a.behind_proxy)
2951
3000
  except WorkspaceError as e:
2952
3001
  raise SystemExit(f"serve: {e}")
3002
+ except TlsError as e:
3003
+ raise SystemExit(f"serve: HTTPS is on for this workspace but {e}. Put the files right, or: gea tls --workspace \"{a.workspace}\" --off")
2953
3004
  n_users = svc.app.users.count()
2954
3005
  print(f"gea {__version__}: serving workspace '{svc.ws.manifest['name']}' at {svc.url}")
2955
3006
  print(f" code: {os.path.dirname(os.path.abspath(__file__))} (an installed copy serves the files it was installed from; pip install -e <clone> follows the clone)")
2956
3007
  print(f" patches: {len(svc.app.patches.store.list())} defined, {len([s for s in svc.app.patches.states() if s['status'] != 'STOPPED'])} running")
2957
3008
  if n_users == 0:
2958
3009
  print(" no accounts yet: the page will ask for the first administrator's name and password (one time), or run: gea users --workspace ... --action add --role admin --name ...")
2959
- if a.host not in ("127.0.0.1", "localhost", "::1") and not a.behind_proxy:
2960
- print(" NOTE: bound to a network address without --behind-proxy: traffic is plain HTTP. Put TLS in front (deploy/README.md) for anything beyond a trusted LAN.")
3010
+ if svc.tls_status:
3011
+ ts_ = svc.tls_status
3012
+ f_ = ts_.get("facts") or {}
3013
+ print(f" HTTPS ({ts_['state']}): {ts_['detail']}")
3014
+ print(f" names {', '.join(f_.get('names') or [])}; SHA-256 {f_.get('sha256_fingerprint')}")
3015
+ if f_.get("self_signed"):
3016
+ print(f" the certificate to hand out (never the key): {ts_['cert']} - or download it from the Administration page")
3017
+ elif a.host not in ("127.0.0.1", "localhost", "::1") and not a.behind_proxy:
3018
+ print(" NOTE: bound to a network address in plain HTTP. Turn HTTPS on (gea tls --workspace ... --site-cert/--site-key, or --self-signed),")
3019
+ print(" or put the site's TLS proxy in front (--behind-proxy; deploy/README.md), for anything beyond a trusted LAN.")
2961
3020
  print(" Ctrl+C stops it; every action is in records/audit.jsonl, every start and stop in records/runlog.jsonl")
2962
3021
  print(" the control panel's Stop and Restart end this run with a stated code: 0 stop, 86 start me again -")
2963
3022
  print(" the launcher reads it, and on anything but 86 it hands the window to a PowerShell prompt, never a Python one")
@@ -12239,6 +12239,228 @@ def section_cw_decline(tmp: str) -> None:
12239
12239
  + (f" [failed: {', '.join(failed4)}]" if failed4 else ""))
12240
12240
 
12241
12241
 
12242
+ def section_cx_https(tmp: str) -> None:
12243
+ """Section CX - HTTPS: the self-signed certificate made by the standard library and read back; the service speaking
12244
+ TLS 1.2 or later with it - the Secure cookie, HSTS, a plain request sent to HTTPS, an untrusting client refused while
12245
+ the service keeps answering; the site's own certificate read where it lives; the expiry alert, the doctor, a broken
12246
+ pair that does not start, the CLI; the key in no dossier and from no route; the page and the help."""
12247
+ import http.client
12248
+ import socket as _so
12249
+ import ssl as _ssl
12250
+ import subprocess as _sp
12251
+ import urllib.error
12252
+ import zipfile as _zf
12253
+ from datetime import datetime as _dt, timedelta as _td, timezone as _tz
12254
+ from . import __version__ as _ver
12255
+ from . import alerts as AL
12256
+ from . import doctor as DR
12257
+ from . import dossier as DZ
12258
+ from . import helplib as H
12259
+ from . import tls as TLS
12260
+ from .service import Service, Users
12261
+ from .workspace import Workspace
12262
+ pkg = Path(__file__).parent
12263
+ d = Path(tmp, "https"); d.mkdir()
12264
+
12265
+ # -- CX1 the certificate by hand ----------------------------------------------------------------------------------
12266
+ key = TLS.rsa_key(2048)
12267
+ sig = TLS.sign_pkcs1_sha256(key, b"pad 3")
12268
+ cert, kpem, facts = TLS.self_signed(["gea-pad3", "gea-pad3.example.local", "10.20.0.15", "localhost", "127.0.0.1"], days=400)
12269
+ (d / "a.crt").write_bytes(cert); (d / "a.key").write_bytes(kpem)
12270
+ der = _ssl.PEM_cert_to_DER_cert(cert.decode("ascii"))
12271
+ nb = _dt.strptime(facts["not_before"], "%Y-%m-%dT%H:%M:%SZ"); na = _dt.strptime(facts["not_after"], "%Y-%m-%dT%H:%M:%SZ")
12272
+ cert_b, key_b, _ = TLS.self_signed(["other"], days=5)
12273
+ (d / "b.crt").write_bytes(cert_b); (d / "b.key").write_bytes(key_b)
12274
+ (d / "enc.key").write_bytes(b"-----BEGIN ENCRYPTED PRIVATE KEY-----\nMIIB\n-----END ENCRYPTED PRIVATE KEY-----\n")
12275
+
12276
+ def refused(fn, *a, **k):
12277
+ try:
12278
+ fn(*a, **k)
12279
+ except TLS.TlsError as e:
12280
+ return str(e)
12281
+ return ""
12282
+ c1 = {
12283
+ "rsa": key["n"].bit_length() == 2048 and key["e"] == 65537 and pow(pow(12345, key["e"], key["n"]), key["d"], key["n"]) == 12345
12284
+ and TLS.verify_pkcs1_sha256(key["n"], key["e"], b"pad 3", sig) and not TLS.verify_pkcs1_sha256(key["n"], key["e"], b"pad 4", sig),
12285
+ "facts": facts["names"] == ["gea-pad3", "gea-pad3.example.local", "10.20.0.15", "localhost", "127.0.0.1"] and facts["subject"] == "gea-pad3"
12286
+ and facts["self_signed"] and facts["self_signature_ok"] and facts["ca"] is False and facts["bits"] == 2048 and facts["key"] == "RSA"
12287
+ and abs((na - nb).total_seconds() - (400 * 86400 + 300)) < 2
12288
+ and facts["sha256_fingerprint"].replace(":", "").lower() == __import__("hashlib").sha256(der).hexdigest(),
12289
+ "loads": isinstance(TLS.server_context(str(d / "a.crt"), str(d / "a.key")), _ssl.SSLContext)
12290
+ and TLS.server_context(str(d / "a.crt"), str(d / "a.key")).minimum_version == _ssl.TLSVersion.TLSv1_2,
12291
+ "refused": all(refused(*x) for x in ((TLS.self_signed, []), (TLS.self_signed, ["x"], 826), (TLS.self_signed, ["x"], 30, 1024),
12292
+ (TLS.self_signed, ["bad name!"])))
12293
+ and "does not belong" in refused(TLS.server_context, str(d / "a.crt"), str(d / "b.key"))
12294
+ and "passphrase" in refused(TLS.server_context, str(d / "a.crt"), str(d / "enc.key"))
12295
+ and "not there" in refused(TLS.server_context, str(d / "nope.crt"), str(d / "a.key"))
12296
+ and "no certificate" in refused(TLS.read_certificate, b"not a certificate"),
12297
+ }
12298
+ failed1 = [k for k, v in c1.items() if not v]
12299
+ ok(not failed1,
12300
+ "CX1 the self-signed certificate is made by the standard library: an RSA key (e 65537; a PKCS #1 v1.5 SHA-256 signature that verifies, and not for "
12301
+ "other bytes); an X.509 v3 server certificate with every name and address as a subject alternative name, its validity, not an authority, its own "
12302
+ "signature good, its SHA-256 fingerprint; loaded by Python's TLS with 1.2 as the floor; no names, more than 825 days, a 1024-bit key, a bad name, a key "
12303
+ "that is not the certificate's, a key with a passphrase and a missing file refused, each saying why" + (f" - failed: {failed1}" if failed1 else ""))
12304
+
12305
+ # -- CX2 the service over HTTPS ------------------------------------------------------------------------------------
12306
+ wsp = str(Path(tmp, "ws_cx")); ws = Workspace.create(wsp, "HTTPS Site", actor="tester")
12307
+ users = Users(str(Path(wsp, "users.json")))
12308
+ users.add("adm", "admin-pass-123", "admin"); users.add("v", "viewer-pass-1", "viewer")
12309
+ st0 = TLS.status(ws)
12310
+ made = TLS.make_self_signed(ws, "tester", ["localhost", "127.0.0.1"], days=400)
12311
+ svc = Service(wsp, host="127.0.0.1", port=0, scheduler=False).start()
12312
+ port = svc.port
12313
+ trust = _ssl.create_default_context(cafile=str(Path(wsp, TLS.SELF_CERT)))
12314
+
12315
+ def req(method, path, body=None, cookie=None, host="127.0.0.1", ctx=trust):
12316
+ c = http.client.HTTPSConnection(host, port, context=ctx, timeout=30)
12317
+ h = {"Content-Type": "application/json", "X-GEA-Action": "1"} if method == "POST" else {}
12318
+ if cookie:
12319
+ h["Cookie"] = cookie
12320
+ c.request(method, path, body=(json.dumps(body) if body is not None else None), headers=h)
12321
+ r = c.getresponse()
12322
+ data = r.read()
12323
+ ver = c.sock.version() if c.sock else None
12324
+ c.close()
12325
+ return r.status, dict(r.getheaders()), data, ver
12326
+
12327
+ def plain(path, host_header):
12328
+ c = http.client.HTTPConnection("127.0.0.1", port, timeout=30)
12329
+ c.request("GET", path, headers={"Host": host_header})
12330
+ r = c.getresponse()
12331
+ out = (r.status, r.getheader("Location"), r.read())
12332
+ c.close()
12333
+ return out
12334
+ try:
12335
+ idle = _so.create_connection(("127.0.0.1", port), timeout=30) # connects and says nothing
12336
+ s_s, h_s, _, ver = req("GET", "/api/session")
12337
+ s_l, h_l, b_l, _ = req("POST", "/api/login", {"name": "adm", "password": "admin-pass-123"})
12338
+ ck = h_l.get("Set-Cookie", "").split(";")[0]
12339
+ s_v, h_v, _, _ = req("POST", "/api/login", {"name": "v", "password": "viewer-pass-1"})
12340
+ vk = h_v.get("Set-Cookie", "").split(";")[0]
12341
+ p301 = plain("/api/overview?x=1", f"127.0.0.1:{port}")
12342
+ p400 = plain("/", "elsewhere.example")
12343
+ try:
12344
+ urllib.request.urlopen(svc.url + "api/session", context=_ssl.create_default_context(), timeout=30)
12345
+ untrusted = "accepted"
12346
+ except urllib.error.URLError as e:
12347
+ untrusted = str(e.reason)
12348
+ wrong_name = _ssl.create_default_context(cafile=str(Path(wsp, TLS.SELF_CERT)))
12349
+ try:
12350
+ with _so.create_connection(("127.0.0.1", port), timeout=30) as raw:
12351
+ with wrong_name.wrap_socket(raw, server_hostname="gea-other.example"):
12352
+ pass
12353
+ name_check = "accepted"
12354
+ except _ssl.SSLError as e:
12355
+ name_check = str(e)
12356
+ s_after, _, _, _ = req("GET", "/api/session")
12357
+ idle.close()
12358
+ # -- CX3 the API ------------------------------------------------------------------------------------------------
12359
+ s_ta, _h, ta_b, _ = req("GET", "/api/tls", cookie=ck)
12360
+ ta = json.loads(ta_b)
12361
+ s_tv, _, _, _ = req("GET", "/api/tls", cookie=vk)
12362
+ s_cv, h_cv, pem_v, _ = req("GET", "/api/tls/certificate", cookie=vk)
12363
+ s_pv, _, _, _ = req("POST", "/api/tls", {"action": "off"}, cookie=vk)
12364
+ s_mm, _, b_mm, _ = req("POST", "/api/tls", {"action": "site", "cert": str(d / "a.crt"), "key": str(d / "b.key")}, cookie=ck)
12365
+ s_bad, _, _, _ = req("POST", "/api/tls", {"action": "nonsense"}, cookie=ck)
12366
+ s_ss, _, b_ss, _ = req("POST", "/api/tls", {"action": "self-signed", "names": "localhost 127.0.0.1", "days": 300}, cookie=ck)
12367
+ s_t2, _, t2, _ = req("GET", "/api/tls", cookie=ck)
12368
+ t2 = json.loads(t2)
12369
+ finally:
12370
+ svc.stop()
12371
+ c2 = {
12372
+ "off_first": st0["state"] == "OFF" and made["state"] == "OK" and made["mode"] == "self-signed",
12373
+ "url": svc.url == f"https://127.0.0.1:{port}/",
12374
+ "tls": s_s == 200 and ver in ("TLSv1.2", "TLSv1.3") and "max-age=" in h_s.get("Strict-Transport-Security", ""),
12375
+ "cookie": s_l == 200 and s_v == 200 and "Secure" in h_l.get("Set-Cookie", "") and "HttpOnly" in h_l.get("Set-Cookie", ""),
12376
+ "plain": p301[0] == 301 and p301[1] == f"https://127.0.0.1:{port}/api/overview?x=1" and p400[0] == 400 and b"speaks HTTPS" in p400[2],
12377
+ "refused": "CERTIFICATE_VERIFY_FAILED" in untrusted and ("mismatch" in name_check.lower() or "doesn't match" in name_check or "not valid" in name_check),
12378
+ "still": s_after == 200 and svc.httpd.tls_refused >= 2,
12379
+ }
12380
+ failed2 = [k for k, v in c2.items() if not v]
12381
+ ok(not failed2,
12382
+ "CX2 with HTTPS on, the service speaks TLS 1.2 or later at an https:// address: a client that trusts the certificate signs in and the cookie is Secure "
12383
+ "and HttpOnly, with Strict-Transport-Security on every answer; plain HTTP on the same port is sent to the same address over HTTPS when it names the "
12384
+ "machine by a name the certificate carries, and told to use HTTPS otherwise; a client that does not trust the certificate, or reaches it by another "
12385
+ "name, is refused - and a silent connection and the refusals do not stop the service answering" + (f" - failed: {failed2}" if failed2 else ""))
12386
+ page_src = (pkg / "web" / "app.html").read_text(encoding="utf-8")
12387
+ adm_view = page_src[page_src.index("VIEWS.admin = "):page_src.index("VIEWS.help = ")]
12388
+ audit = Path(ws.audit_path).read_text(encoding="utf-8")
12389
+ c3 = {
12390
+ "status": s_ta == 200 and ta["state"] == "OK" and ta["serving"]["https"] and ta["restart_needed"] is False
12391
+ and ta["facts"]["sha256_fingerprint"] == made["facts"]["sha256_fingerprint"],
12392
+ "roles": s_tv == 403 and s_pv == 403 and s_bad == 400,
12393
+ "certificate": s_cv == 200 and pem_v.startswith(b"-----BEGIN CERTIFICATE-----") and b"PRIVATE KEY" not in pem_v
12394
+ and TLS.read_certificate(pem_v)["sha256_fingerprint"] == made["facts"]["sha256_fingerprint"] and "attachment" in h_cv.get("Content-Disposition", ""),
12395
+ "change": s_mm == 400 and b"does not belong" in b_mm and s_ss == 200 and json.loads(b_ss)["restart_needed"] is True
12396
+ and t2["restart_needed"] is True and t2["serving"]["https"] and t2["facts"]["sha256_fingerprint"] != made["facts"]["sha256_fingerprint"],
12397
+ "audit": audit.count('"tls.self_signed"') == 2,
12398
+ "page": all(x in adm_view for x in ("/api/tls", "tlsSite", "tlsSelf", "/api/tls/certificate", "const cStop = $('#ctlStop')", "cAuto.onchange"))
12399
+ and "const cStop" not in page_src[page_src.index("VIEWS.prefs = "):page_src.index("VIEWS.search = ")],
12400
+ }
12401
+ failed3 = [k for k, v in c3.items() if not v]
12402
+ ok(not failed3,
12403
+ "CX3 the HTTPS card (the administrator role): how it stands and what this run serves; the certificate - public - downloads for any signed-in user "
12404
+ "and carries no key; a viewer may not change it; a key that is not the certificate's is refused; a new certificate is kept and says it needs a "
12405
+ "restart; each change is audited; the operations-control buttons on the Administration page are wired there"
12406
+ + (f" - failed: {failed3}" if failed3 else ""))
12407
+
12408
+ # -- CX4 the site's certificate, the expiry alert, a broken pair, the CLI, the key kept in ----------------------------
12409
+ site_dir = Path(tmp, "site_certs"); site_dir.mkdir()
12410
+ sc, sk, sf = TLS.self_signed(["gea-pad3.example.local", "127.0.0.1"], days=60, org="Example Operator IT")
12411
+ (site_dir / "gea.crt").write_bytes(sc); (site_dir / "gea.key").write_bytes(sk)
12412
+ st_site = TLS.use_site_certificate(ws, "tester", str(site_dir / "gea.crt"), str(site_dir / "gea.key"))
12413
+ copied = sorted(p.name for p in Path(wsp, TLS.TLS_DIR).iterdir())
12414
+ exp_at = _dt.strptime(sf["not_after"], "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=_tz.utc)
12415
+ st_exp = TLS.status(ws, exp_at - _td(days=10))
12416
+ r_exp = AL.check_conditions(ws, None, now=exp_at - _td(days=10), force=True)
12417
+ TLS.make_self_signed(ws, "tester", ["localhost", "127.0.0.1"], days=400)
12418
+ r_clr = AL.check_conditions(ws, None, now=_dt.now(_tz.utc), force=True)
12419
+ os.remove(Path(wsp, TLS.SELF_KEY))
12420
+ st_broken = TLS.status(ws)
12421
+ doc = DR.check_workspace(wsp, "127.0.0.1", 0)
12422
+ try:
12423
+ Service(wsp, host="127.0.0.1", port=0, scheduler=False)
12424
+ start_broken = "started"
12425
+ except TLS.TlsError as e:
12426
+ start_broken = str(e)
12427
+ cli_off = _sp.run([sys.executable, "-m", "gea", "tls", "--workspace", wsp, "--off"], capture_output=True, text=True, cwd=str(pkg.parent))
12428
+ svc_plain = Service(wsp, host="127.0.0.1", port=0, scheduler=False)
12429
+ url_plain = svc_plain.url
12430
+ svc_plain.httpd.server_close()
12431
+ out_crt = str(d / "handed_out.crt")
12432
+ cli_ss = _sp.run([sys.executable, "-m", "gea", "tls", "--workspace", wsp, "--self-signed", "--name", "gea-pad3", "--name", "10.20.0.15", "--days", "90",
12433
+ "--export-cert", out_crt], capture_output=True, text=True, cwd=str(pkg.parent))
12434
+ cli_st = _sp.run([sys.executable, "-m", "gea", "tls", "--workspace", wsp, "--json"], capture_output=True, text=True, cwd=str(pkg.parent))
12435
+ cli_bad = _sp.run([sys.executable, "-m", "gea", "tls", "--workspace", wsp, "--site-cert", str(site_dir / "gea.crt")], capture_output=True, text=True, cwd=str(pkg.parent))
12436
+ __import__("gea.frac_hits", fromlist=["write_report"]).write_report(ws, _ver, "tester")
12437
+ dz = DZ.build(ws, "tester", _ver, None, include_sources=True)
12438
+ zp = Path(ws.reports_dir, dz["path"])
12439
+ with _zf.ZipFile(zp) as z:
12440
+ names = z.namelist()
12441
+ leaked = [n for n in names if b"PRIVATE KEY" in z.read(n)]
12442
+ key_text = Path(wsp, TLS.SELF_KEY).read_bytes()
12443
+ c4 = {
12444
+ "site": st_site["state"] == "OK" and st_site["mode"] == "site" and st_site["cert"] == str(site_dir / "gea.crt")
12445
+ and st_site["facts"]["issuer"] == "gea-pad3.example.local" and "self_signed_key.pem" in copied and "gea.key" not in copied,
12446
+ "expiring": st_exp["state"] == "EXPIRING" and [e["event"] for e in r_exp if e["event"] == "tls.expiring"] == ["tls.expiring"]
12447
+ and any(e["event"] == "condition.cleared" and e.get("cleared") == "tls.expiring" for e in r_clr) and "tls.expiring" in AL.EVENTS,
12448
+ "broken": st_broken["state"] == "BROKEN" and any(f["level"] == "block" and "HTTPS BROKEN" in f["what"] for f in doc) and "not there" in start_broken,
12449
+ "cli": cli_off.returncode == 0 and "HTTPS: OFF" in cli_off.stdout and url_plain.startswith("http://127.0.0.1:")
12450
+ and cli_ss.returncode == 0 and "gea-pad3, 10.20.0.15" in cli_ss.stdout and "next start" in cli_ss.stdout
12451
+ and Path(out_crt).read_bytes().startswith(b"-----BEGIN CERTIFICATE-----") and b"PRIVATE" not in Path(out_crt).read_bytes()
12452
+ and cli_st.returncode == 0 and json.loads(cli_st.stdout)["state"] == "OK" and cli_bad.returncode != 0 and "go together" in (cli_bad.stderr + cli_bad.stdout),
12453
+ "kept_in": not leaked and not any(n.startswith("keys/tls") or n.endswith(".pem") for n in names) and b"PRIVATE KEY" in key_text,
12454
+ "help": not H.check("https") and any(t["topic"] == "https" for t in H.topics()) and "gea help https" in (pkg / "help" / "upkeep.md").read_text(encoding="utf-8"),
12455
+ }
12456
+ failed4 = [k for k, v in c4.items() if not v]
12457
+ ok(not failed4,
12458
+ "CX4 the site's own certificate is used where it lives (not copied in); in its last 30 days it reads EXPIRING and raises tls.expiring once, cleared "
12459
+ "when a new one is in place; with its key gone the doctor says BLOCK and the service does not start (never plain HTTP by surprise) until "
12460
+ "`gea tls --off`; `gea tls --self-signed --name ... --export-cert` makes one and writes the certificate alone; --site-cert without its key is refused; "
12461
+ "no dossier carries the key; the help page" + (f" - failed: {failed4}" if failed4 else ""))
12462
+
12463
+
12242
12464
  def PR_read_ok(path) -> bool:
12243
12465
  from . import pdf_report as PR
12244
12466
  try:
@@ -12351,6 +12573,7 @@ def main() -> int:
12351
12573
  section_cu_pore_pressure(tmp)
12352
12574
  section_cv_torque_drag(tmp)
12353
12575
  section_cw_decline(tmp)
12576
+ section_cx_https(tmp)
12354
12577
  if _FAILS:
12355
12578
  print(f"[ACCEPTANCE] {len(_FAILS)} FAILURES ({_PASS} passed):")
12356
12579
  for f in _FAILS:
@@ -17,7 +17,10 @@ worth an event, for the conditions the operator does not watch by hand:
17
17
  attached);
18
18
  - `frac.hit` - a fracturing stage reads as a FRAC HIT on a monitored well's pressure (the Frac hits tab; checked every
19
19
  15 minutes, once per stage and well);
20
- - `condition.cleared` - one of motor.due, penetrator.outside, dossier.changed or invoice.differs is no longer so.
20
+ - `tls.expiring` - the dashboard's HTTPS certificate is in its last 30 days, or has expired (checked every 15
21
+ minutes while HTTPS is on);
22
+ - `condition.cleared` - one of motor.due, penetrator.outside, dossier.changed, invoice.differs or tls.expiring is no
23
+ longer so.
21
24
 
22
25
  Every alert is written to `records/alerts.jsonl` whether or not any notification channel is set up - the Alarms
23
26
  tab lists them - and is handed to the notification rules, which send it where the site said (a rule per event
@@ -32,7 +35,7 @@ from datetime import datetime, timezone
32
35
  from typing import Dict, List, Optional
33
36
 
34
37
  EVENTS = ('seismic.protocol', 'seismic.event', 'seismic.watch_down', 'seismic.watch_up', 'motor.due', 'penetrator.outside',
35
- 'dossier.changed', 'invoice.differs', 'frac.hit', 'condition.cleared')
38
+ 'dossier.changed', 'invoice.differs', 'frac.hit', 'tls.expiring', 'condition.cleared')
36
39
  CONDITIONS_EVERY_S = 900 # motors and penetrators: every 15 minutes
37
40
  DOSSIERS_EVERY_S = 86400 # dossiers: once a day
38
41
  _LOCK = threading.Lock()
@@ -172,6 +175,16 @@ def check_conditions(ws, notifier, now: Optional[datetime] = None, force: bool =
172
175
  st['frac_hits_seen'] = sorted(done)
173
176
  except Exception as ex:
174
177
  last['frac_error'] = f'{type(ex).__name__}: {ex}'
178
+ try:
179
+ from . import tls as TLS
180
+ ts = TLS.status(ws, now)
181
+ if ts.get('configured') or 'tls.expiring' in st['conditions']:
182
+ f_ = ts.get('facts') or {}
183
+ _transition(ws, notifier, st, 'tls.expiring', ts['state'] in ('EXPIRING', 'EXPIRED'),
184
+ {'event': 'tls.expiring', 'what': f"the dashboard's HTTPS certificate {f_.get('subject', '')}".strip(),
185
+ 'not_after': f_.get('not_after'), 'days_left': ts.get('days_left'), 'detail': ts['detail']}, now, raised)
186
+ except Exception as ex:
187
+ last['tls_error'] = f'{type(ex).__name__}: {ex}'
175
188
  for key in [k for k in st['conditions'] if k.split(':')[0] in ('motor.due', 'penetrator.outside') and k not in seen]:
176
189
  st['conditions'].pop(key, None) # the run or the record was removed: nothing left to clear
177
190
  if due('dossiers', DOSSIERS_EVERY_S):
@@ -253,6 +253,18 @@ def check_workspace(path: str, host: str = '127.0.0.1', port: int = 8765) -> Lis
253
253
  else:
254
254
  f.append(_finding('block', f'port {port} on {host} is already in use (another gea serve, or another program)',
255
255
  f'stop the other service, or start with --port {port + 1}'))
256
+ try:
257
+ from . import tls as TLS
258
+ ts = TLS.status(ws)
259
+ lvl = {'OFF': 'info', 'OK': 'ok', 'EXPIRING': 'warn', 'EXPIRED': 'warn', 'NOT_YET_VALID': 'warn', 'BROKEN': 'block'}[ts['state']]
260
+ fix = {'OFF': f'for the site network: gea tls --workspace "{path}" --site-cert <the .crt or .pem> --site-key <its key>, or --self-signed',
261
+ 'EXPIRING': f'gea tls --workspace "{path}" shows the certificate; renew it (or --self-signed for a new one) and restart',
262
+ 'EXPIRED': f'gea tls --workspace "{path}" --self-signed makes a new one; a site certificate is renewed by the site',
263
+ 'NOT_YET_VALID': "set this machine's clock",
264
+ 'BROKEN': f'put the certificate and key files right, or turn HTTPS off: gea tls --workspace "{path}" --off'}.get(ts['state'], '')
265
+ f.append(_finding(lvl, f"HTTPS {ts['state']}: {ts['detail']}", fix))
266
+ except Exception as e:
267
+ f.append(_finding('warn', f'the HTTPS setting could not be read: {e}'))
256
268
  prev = _SV.previous_run(ws.path)
257
269
  if prev['status'] == 'UNCLEAN':
258
270
  f.append(_finding('warn', 'the last run of the control panel was killed rather than stopped: ' + prev['detail'],
@@ -6,7 +6,7 @@
6
6
 
7
7
  **How the watch reads the catalogue**: it runs in the service's background poller, so only while `gea serve` (the control panel) is running. An operation is under way when a declared operation (Operations & Seismicity, or the declarations sheet's `[operations]`) has started and has not ended, at a well with a surface position. A poll reads the USGS real-time summary feed of the past day, all magnitudes, updated every minute - the feed USGS asks automated programs to use - and keeps the earthquakes inside the circle around the operating wells (out to the screening distance, 20 km by default, beyond the farthest). On the first poll, or after a gap of a day or more, it asks the FDSN event service for the whole gap instead (from the operation's start, at most 30 days back). USGS revises magnitudes and locations; the newest version wins, and a revision that reaches a higher protocol level is a new alert.
8
8
 
9
- **The alerts**: `seismic.protocol` (a protocol level reached, once per level), `seismic.event` (any earthquake within the association radius during or after an operation, once), `seismic.watch_down` (no answer from the catalogue for three intervals while an operation runs) and `seismic.watch_up`; `motor.due` (checked every 15 minutes), `penetrator.outside` (every 15 minutes), `dossier.changed` (the newest dossier of each well and of the pad, re-checked daily), `invoice.differs` (a well's invoice cross-check finds a day that differs; checked daily and when a file is attached), `frac.hit` (a fracturing stage reads as a frac hit on a monitored well's pressure; checked every 15 minutes, once per stage and well), and `condition.cleared` when one of motor.due, penetrator.outside, dossier.changed or invoice.differs ends.
9
+ **The alerts**: `seismic.protocol` (a protocol level reached, once per level), `seismic.event` (any earthquake within the association radius during or after an operation, once), `seismic.watch_down` (no answer from the catalogue for three intervals while an operation runs) and `seismic.watch_up`; `motor.due` (checked every 15 minutes), `penetrator.outside` (every 15 minutes), `dossier.changed` (the newest dossier of each well and of the pad, re-checked daily), `invoice.differs` (a well's invoice cross-check finds a day that differs; checked daily and when a file is attached), `frac.hit` (a fracturing stage reads as a frac hit on a monitored well's pressure; checked every 15 minutes, once per stage and well), `tls.expiring` (the control panel's HTTPS certificate in its last 30 days or expired; every 15 minutes while HTTPS is on - `gea help https`), and `condition.cleared` when one of motor.due, penetrator.outside, dossier.changed, invoice.differs or tls.expiring ends.
10
10
 
11
11
  **Getting them to a person**: every alert is written to `records/alerts.jsonl` and shown on the Alarms tab. To have it sent, set up notifications (Administration, Notifications: load the example, edit, validate and commit) with an email (SMTP) or webhook channel and a rule naming the alert, for example `{"event": "seismic.protocol", "channels": ["mail"]}`; the password stays in an environment variable named by `password_env`. A text message: most carriers deliver email sent to the phone's email-to-text address, or a webhook to a messaging service. The `quiet_s` window keeps one alert from repeating.
12
12
 
@@ -0,0 +1,19 @@
1
+ # HTTPS
2
+
3
+ > How browsers on the site network reach the control panel without anything in plain text: the site's own certificate (best: the company's certificate authority, which managed computers already trust) or a self-signed one made here; the names it must carry; handing it out; renewal and the expiry alert.
4
+
5
+ **The command**: Administration -> **HTTPS** (the administrator role): how it stands, the certificate's names, validity, issuer and SHA-256 fingerprint, **Download the certificate**, and under **Change it**: **Use the site's certificate** (the certificate file and its private key, as paths on the machine running the service), **Make a self-signed certificate** (the names it is reached by, the days), **Turn HTTPS off**. A change takes effect at the next start: **Restart** under Operations control. From a terminal: `gea tls --workspace C:\site` prints how it stands; `--site-cert C:\certs\gea-pad3.crt --site-key C:\certs\gea-pad3.key` uses the site's; `--self-signed --name gea-pad3 --name gea-pad3.example.local --name 10.20.0.15` makes one (with no `--name`: this machine's names, localhost and 127.0.0.1); `--off`; `--export-cert C:\certs\gea-site.crt` writes the certificate (never the key) for the site's computers. Then `gea serve --workspace C:\site --host 0.0.0.0` (or the kit's start-dashboard) and open `https://gea-pad3:8765/`.
6
+
7
+ **What it needs**: for the site's certificate, PEM files: the server certificate first (then any chain), and its private key without a passphrase (the service starts unattended); a `.pfx` is exported to PEM first (`openssl pkcs12 -in gea.pfx -out gea.crt -nokeys` and `-nocerts -nodes -out gea.key`), and the key file is readable by the service's account only. The certificate must carry every name the browsers use - the machine's short name, its full name, its address - as a subject alternative name. The site's files are read where they are, at every start: a renewal put in their place needs only a restart.
8
+
9
+ **How it is read**: TLS 1.2 is the floor (TLS 1.3 when both ends have it). With HTTPS on, the session cookie is marked Secure, every response tells the browser to use only HTTPS from then on (Strict-Transport-Security, 180 days), and a plain `http://` request on the same port is sent to the same address over HTTPS when it named the machine by a name the certificate carries (otherwise it is told to use HTTPS). Each connection's handshake runs in its own thread, so a scanner or an old client cannot hold up the others. The self-signed certificate is made by this program alone: an RSA key of 2048 bits (3072 on request), an X.509 v3 certificate for a TLS server (not a certificate authority), signed with SHA-256, valid for at most 825 days (Apple's systems refuse a longer one even when trusted by hand). A browser warns about a self-signed certificate until the computer trusts it: by group policy across the site, or by hand (open the downloaded `.crt`, Install Certificate, Local Machine, Trusted Root Certification Authorities) - and the fingerprint shown there is compared with the one on the HTTPS card before it is trusted. A workspace whose certificate files are missing, unreadable or not a pair does not start (`gea doctor` says BLOCK): a site that turned HTTPS on is never served in plain text by surprise. A site already running a reverse proxy can keep TLS there instead (`--behind-proxy`, `gea help upkeep`).
10
+
11
+ **What it writes**: the setting in `workspace.json` (`control.tls`: the mode, the two files, when and by whom) with the audit lines `tls.site_certificate`, `tls.self_signed` and `tls.off`; for a self-signed certificate, `keys\tls\self_signed_certificate.pem` and `keys\tls\self_signed_key.pem`. The key is never in a report, an export, an evidence pack or a dossier, and the service never sends it; the backup copies it with the rest of the workspace, like the dossier signing key. The alert `tls.expiring` when the certificate is in its last 30 days or has expired, checked every 15 minutes, and cleared when a new one is in place.
12
+
13
+ **The number to check**: the SHA-256 fingerprint on the HTTPS card against the one the browser shows for the page (the padlock -> the certificate) - the same, or it is not this machine answering; the days left.
14
+
15
+ **What this page will not call a measurement**: a padlock is not proof of who is answering unless the computer trusts the certificate for a reason - the site's authority, or a fingerprint someone compared. A self-signed certificate encrypts; it does not identify until it is trusted that way.
16
+
17
+ **Sources**: RFC 8446 (TLS 1.3) and RFC 5246 (TLS 1.2); RFC 5280 (the X.509 certificate and its extensions); RFC 8017 (RSA, PKCS #1 v1.5 signatures); RFC 6797 (Strict-Transport-Security); Apple, "Requirements for trusted certificates in iOS 13 and macOS 10.15" (HT210176: a server certificate valid for at most 825 days, with its names as subject alternative names and the server-authentication key usage).
18
+
19
+ Related: `gea help upkeep`, `gea help alerts`, `gea help doctor`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  > Keeping a site running for years: logs, jobs, sessions, the proxy, the load it can carry.
4
4
 
5
- **The command**: `gea housekeeping --workspace C:\site` (a dry run that lists what it would do) and `--apply`; schedule it daily from Administration (preset "housekeeping"). `gea loadtest --patches 16 --seconds 60 --with-service` before a site goes live. Administration -> Live sessions lists who is signed in from where and revokes one or all of a user's sessions; "Your preferences" has "Sign out everywhere else". `deploy/README.md`, `Caddyfile` and `nginx-gea.conf` put TLS in front; start the service with `--behind-proxy`.
5
+ **The command**: `gea housekeeping --workspace C:\site` (a dry run that lists what it would do) and `--apply`; schedule it daily from Administration (preset "housekeeping"). `gea loadtest --patches 16 --seconds 60 --with-service` before a site goes live. Administration -> Live sessions lists who is signed in from where and revokes one or all of a user's sessions; "Your preferences" has "Sign out everywhere else". HTTPS without a proxy: `gea help https` (the service speaks TLS itself with the site's certificate or a self-signed one). Or, where the site already runs one, `deploy/README.md`, `Caddyfile` and `nginx-gea.conf` put TLS in a reverse proxy in front; start the service with `--behind-proxy`.
6
6
 
7
7
  **What it writes**: housekeeping segments the append-only logs (`audit.jsonl.<stamp>`, `alarm_events.jsonl.<stamp>`; the fresh alarm log starts at the carried PROCESSED watermark so nothing is re-processed), removes finished job folders older than 30 days beyond the newest 500, removes live recordings older than 90 days except the newest stream file per well, and audits the run. The load test leaves nothing behind.
8
8
 
@@ -10,4 +10,4 @@
10
10
 
11
11
  **What this page will not call a measurement**: a segmented log is not a deleted one - every segment is the record. Deletion is only ever of finished job folders and old recordings whose samples are already folded into the stream.
12
12
 
13
- Related: `gea help site`, `gea help patches`.
13
+ Related: `gea help site`, `gea help patches`, `gea help https`.
@@ -84,6 +84,7 @@ INDEX: List[tuple] = [
84
84
  ('doctor', 'Which code is running', 'gea doctor and the serve start-up gate'),
85
85
  ('audit-update', 'Audit / Update', 'the whole audit log with filters, the program against PyPI, every report against its source'),
86
86
  ('soak', 'The soak', 'the patches through link outages for hours: reconnection, resume, loss by protocol, the page answering - measured, not modelled'),
87
+ ('https', 'HTTPS', "the control panel over HTTPS on the site network: the site's own certificate or a self-signed one made here, the names it carries, handing it out, renewal and the expiry alert"),
87
88
  ('upkeep', 'Upkeep', 'housekeeping, sessions, the TLS proxy, the load test'),
88
89
  ]
89
90
  # the four lines every page must carry, in this order
@@ -68,7 +68,7 @@ EXAMPLE = {
68
68
  }
69
69
  EVENTS = ('alarm.activated', 'alarm.shelved', 'job.failed', 'patch.down', 'patch.up', 'approval.pending', 'file.imported', 'test',
70
70
  'seismic.protocol', 'seismic.event', 'seismic.watch_down', 'seismic.watch_up', 'motor.due', 'penetrator.outside', 'dossier.changed',
71
- 'condition.cleared', 'pad.summary', 'invoice.differs', 'frac.hit')
71
+ 'condition.cleared', 'pad.summary', 'invoice.differs', 'frac.hit', 'tls.expiring')
72
72
  _LOCK = threading.Lock()
73
73
 
74
74
 
@@ -185,6 +185,8 @@ def _subject(ev: dict) -> str:
185
185
  return f"[GEA] the workspace no longer matches dossier {ev.get('dossier_id', '')}"
186
186
  if k == 'invoice.differs':
187
187
  return f"[GEA] the invoice for {w} differs from the reports and the record: {ev.get('detail', '')}"
188
+ if k == 'tls.expiring':
189
+ return f"[GEA] HTTPS certificate: {ev.get('detail', '')}"
188
190
  if k == 'frac.hit':
189
191
  return f"[GEA] frac hit: stage {ev.get('stage', '')} of {w} on {ev.get('monitor', '')} - {ev.get('detail', '')}"
190
192
  if k == 'pad.summary':