remoteRF-server 1.7.12__tar.gz → 1.7.13__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 (211) hide show
  1. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/PKG-INFO +1 -32
  2. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/README.md +0 -31
  3. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/pyproject.toml +1 -1
  4. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/remote_admin_shell.py +3 -3
  5. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/commands.py +0 -5
  6. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/global_onboarding.py +83 -25
  7. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/grpc_server.py +0 -4
  8. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/reservation.py +40 -166
  9. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/serverrf_cli.py +0 -327
  10. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/PKG-INFO +1 -32
  11. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/SOURCES.txt +0 -3
  12. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_client_sessions.py +100 -0
  13. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_global_onboarding.py +120 -6
  14. remoterf_server-1.7.12/src/remoteRF_server/tools/gist_status.py +0 -218
  15. remoterf_server-1.7.12/src/remoteRF_server/tools/gist_status_testing.py +0 -82
  16. remoterf_server-1.7.12/tests/test_status_push.py +0 -20
  17. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/LICENSE +0 -0
  18. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/setup.cfg +0 -0
  19. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/__init__.py +0 -0
  20. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/__init__.py +0 -0
  21. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/__init__.py +0 -0
  22. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/deployment_capabilities.proto +0 -0
  23. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2.py +0 -0
  24. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/deployment_capabilities_pb2_grpc.py +0 -0
  25. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/federation.proto +0 -0
  26. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/federation_pb2.py +0 -0
  27. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/federation_pb2_grpc.py +0 -0
  28. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/grpc_host_pb2.py +0 -0
  29. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +0 -0
  30. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/grpc_pb2.py +0 -0
  31. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/grpc_pb2_grpc.py +0 -0
  32. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/grpc/v2_codec.py +0 -0
  33. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/__init__.py +0 -0
  34. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/device_schema.py +0 -0
  35. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/pluto_mimo_schema.py +0 -0
  36. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/pluto_schema.py +0 -0
  37. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/schema.py +0 -0
  38. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/idl/v2.py +0 -0
  39. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/__init__.py +0 -0
  40. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/ansi_codes.py +0 -0
  41. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/api_token.py +0 -0
  42. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/db_connection.py +0 -0
  43. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/db_location.py +0 -0
  44. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/list_string.py +0 -0
  45. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/panel.py +0 -0
  46. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/common/utils/process_arg.py +0 -0
  47. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/__init__.py +0 -0
  48. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
  49. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/pluto_diag.py +0 -0
  50. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/pluto_mimo_schema.py +0 -0
  51. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +0 -0
  52. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/pluto_schema.py +0 -0
  53. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/adalm_pluto/poe.py +0 -0
  54. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/bluetooth_diag.py +0 -0
  55. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/gps_diag.py +0 -0
  56. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/hackrf/__init__.py +0 -0
  57. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/hackrf/hackrf_schema.py +0 -0
  58. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/rtl_sdr/__init__.py +0 -0
  59. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_diag.py +0 -0
  60. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/rtl_sdr/rtl_sdr_schema.py +0 -0
  61. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/ti_mmwave/__init__.py +0 -0
  62. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/ti_mmwave/ti_mmwave_schema.py +0 -0
  63. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/__init__.py +0 -0
  64. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/policy.py +0 -0
  65. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/profiles.py +0 -0
  66. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/uhd_manifest.py +0 -0
  67. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/usrp_diag.py +0 -0
  68. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp/usrp_schema.py +0 -0
  69. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp_phaser/__init__.py +0 -0
  70. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/usrp_phaser/usrp_phaser_schema.py +0 -0
  71. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/wifi/__init__.py +0 -0
  72. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/drivers/wifi/wifi_schema.py +0 -0
  73. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/host/__init__.py +0 -0
  74. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/host/host_auth_token.py +0 -0
  75. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/host/host_directory_store.py +0 -0
  76. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/host/host_tunnel_server.py +0 -0
  77. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/__init__.py +0 -0
  78. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/acc_perms.py +0 -0
  79. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/cert_provider.py +0 -0
  80. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/console.py +0 -0
  81. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/control.py +0 -0
  82. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/deployment/__init__.py +0 -0
  83. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/deployment/capabilities.py +0 -0
  84. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/device_manager.py +0 -0
  85. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/diag.py +0 -0
  86. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/direct_path.py +0 -0
  87. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/dynamic_v2.py +0 -0
  88. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/dynamic_v2_bridge.py +0 -0
  89. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/__init__.py +0 -0
  90. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/admin.py +0 -0
  91. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/cli.py +0 -0
  92. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/cluster.py +0 -0
  93. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/contracts.py +0 -0
  94. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/destination.py +0 -0
  95. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/devices.py +0 -0
  96. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/enrollment.py +0 -0
  97. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/global_home.py +0 -0
  98. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/home.py +0 -0
  99. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/home_devices.py +0 -0
  100. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/identity.py +0 -0
  101. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/legacy_state.py +0 -0
  102. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/links.py +0 -0
  103. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/migrations.py +0 -0
  104. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/peers.py +0 -0
  105. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/permissions.py +0 -0
  106. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/principals.py +0 -0
  107. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/runtime.py +0 -0
  108. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/federation/trust.py +0 -0
  109. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/host_direct.py +0 -0
  110. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/host_dynamic_v2.py +0 -0
  111. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/json_output.py +0 -0
  112. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/remote_admin.py +0 -0
  113. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/remote_admin_ssh.py +0 -0
  114. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/rpc_manager.py +0 -0
  115. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/schema_registry.py +0 -0
  116. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/startup_check.py +0 -0
  117. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/user_group_cli.py +0 -0
  118. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/user_group_handler.py +0 -0
  119. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/server/v2_errors.py +0 -0
  120. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/tools/__init__.py +0 -0
  121. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/tools/gen_certs.py +0 -0
  122. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server/tools/introspect_uhd.py +0 -0
  123. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/dependency_links.txt +0 -0
  124. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/entry_points.txt +0 -0
  125. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/requires.txt +0 -0
  126. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/remoteRF_server.egg-info/top_level.txt +0 -0
  127. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/rssi/__init__.py +0 -0
  128. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/rssi/cli.py +0 -0
  129. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/rssi/dongle.py +0 -0
  130. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/rssi/safety.py +0 -0
  131. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/src/rssi/scanner.py +0 -0
  132. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_account_hardening.py +0 -0
  133. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_account_names.py +0 -0
  134. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_account_removal.py +0 -0
  135. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_activity_log.py +0 -0
  136. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_availability.py +0 -0
  137. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_bind_host.py +0 -0
  138. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_cert_provider.py +0 -0
  139. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_cluster.py +0 -0
  140. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_commands.py +0 -0
  141. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_console.py +0 -0
  142. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_contrib_drivers.py +0 -0
  143. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_default_rtl_sdr_schema.py +0 -0
  144. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_default_usrp_schema.py +0 -0
  145. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_default_wifi_schema.py +0 -0
  146. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_deployment_capabilities.py +0 -0
  147. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_detached_device_rpc.py +0 -0
  148. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_device_core_aliases.py +0 -0
  149. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_device_handles.py +0 -0
  150. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_device_init.py +0 -0
  151. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_devices_add_wizard.py +0 -0
  152. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_devices_list.py +0 -0
  153. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_diag.py +0 -0
  154. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_direct_fixed_port.py +0 -0
  155. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_direct_path_server.py +0 -0
  156. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_dynamic_v2.py +0 -0
  157. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_federated_direct_path.py +0 -0
  158. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_federated_dynamic_v2.py +0 -0
  159. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_federation_edges.py +0 -0
  160. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_first_run.py +0 -0
  161. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_d_contracts.py +0 -0
  162. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_e_federation.py +0 -0
  163. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_e_proto.py +0 -0
  164. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_f_enrollment.py +0 -0
  165. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_g1_logging.py +0 -0
  166. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_g_permissions.py +0 -0
  167. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_h_devices.py +0 -0
  168. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gate_i_direct_path.py +0 -0
  169. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_gen_certs.py +0 -0
  170. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_global_accounts_migration.py +0 -0
  171. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_global_home.py +0 -0
  172. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_global_v1_disposal.py +0 -0
  173. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_hackrf_schema.py +0 -0
  174. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_home_device_plane.py +0 -0
  175. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_host_direct.py +0 -0
  176. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_host_directory_store.py +0 -0
  177. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_host_dynamic_v2.py +0 -0
  178. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_idl_schema.py +0 -0
  179. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_json_commands.py +0 -0
  180. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_lab_logins.py +0 -0
  181. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_panel.py +0 -0
  182. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_peer_transport.py +0 -0
  183. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_pluto_compat.py +0 -0
  184. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_pluto_poe.py +0 -0
  185. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_process_arg.py +0 -0
  186. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_proxy_lifecycle.py +0 -0
  187. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_remote_admin.py +0 -0
  188. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_reservation_cancel.py +0 -0
  189. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_reservation_handoff.py +0 -0
  190. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_reservation_policy_b.py +0 -0
  191. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_reservation_timestamps.py +0 -0
  192. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_rpc_manager_batch.py +0 -0
  193. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_rpc_manager_calln.py +0 -0
  194. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_rpc_manager_host_legacy.py +0 -0
  195. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_rpc_manager_rx_n.py +0 -0
  196. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_rtl_sdr_schema.py +0 -0
  197. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_schema_registry.py +0 -0
  198. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_soak_federation.py +0 -0
  199. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_startup_check.py +0 -0
  200. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_ti_mmwave_hardware.py +0 -0
  201. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_ti_mmwave_schema.py +0 -0
  202. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_user_group_cli.py +0 -0
  203. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_b205mini_uhd410_hardware.py +0 -0
  204. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_client_server_hardware.py +0 -0
  205. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_client_server_localhost.py +0 -0
  206. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_n210_client_server_hardware.py +0 -0
  207. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_preload.py +0 -0
  208. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_profiles.py +0 -0
  209. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_remoterf_rx_hardware.py +0 -0
  210. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_usrp_uhd410_hardware.py +0 -0
  211. {remoterf_server-1.7.12 → remoterf_server-1.7.13}/tests/test_wipe.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: remoteRF-server
3
- Version: 1.7.12
3
+ Version: 1.7.13
4
4
  Summary: RemoteRF server-side control package
5
5
  License: GPL-3.0-or-later
6
6
  Classifier: Programming Language :: Python :: 3
@@ -953,37 +953,6 @@ serverrf --host --delete host-name
953
953
  serverrf --host --wipe
954
954
  ```
955
955
 
956
- ## Status (Optional)
957
-
958
- If you desire to pull the server information (ie: live updates of reservation, server usage parameters, etc.)
959
-
960
- It uses GitHub Gists to push the status updates.
961
-
962
- Create public gist on your github. Keep note of the file name: (example)
963
- https://gist.github.com/ethange1/2a35e08a90bf88a70dfe7f42a55685ed
964
-
965
- The last one is the "GIST_ID" part of the URL, e.g. "2a35e08a90bf88a70dfe7f42a55685ed"
966
-
967
- Creating the GitHub PAT Token
968
-
969
- * GitHub → Settings
970
- * Developer settings
971
- * Personal access tokens → Fine-grained tokens
972
- * Generate new token
973
- * Set repository access to "Public Respositories"
974
- * Permissions: enable Gists: Read and write
975
- * Generate + copy the token (you only see it once)
976
-
977
- ```bash
978
- serverrf --gist --set --id <gist_id> --file <filename>
979
- ```
980
-
981
- Run the server normally, and you should see your gist change every minute!
982
-
983
- ```bash
984
- serverrf -s
985
- ```
986
-
987
956
  <!-- todo: -->
988
957
  <!-- Adding plutos via the CLI -->
989
958
  <!-- Testing functionality (disregarding Host pipeline) -->
@@ -920,37 +920,6 @@ serverrf --host --delete host-name
920
920
  serverrf --host --wipe
921
921
  ```
922
922
 
923
- ## Status (Optional)
924
-
925
- If you desire to pull the server information (ie: live updates of reservation, server usage parameters, etc.)
926
-
927
- It uses GitHub Gists to push the status updates.
928
-
929
- Create public gist on your github. Keep note of the file name: (example)
930
- https://gist.github.com/ethange1/2a35e08a90bf88a70dfe7f42a55685ed
931
-
932
- The last one is the "GIST_ID" part of the URL, e.g. "2a35e08a90bf88a70dfe7f42a55685ed"
933
-
934
- Creating the GitHub PAT Token
935
-
936
- * GitHub → Settings
937
- * Developer settings
938
- * Personal access tokens → Fine-grained tokens
939
- * Generate new token
940
- * Set repository access to "Public Respositories"
941
- * Permissions: enable Gists: Read and write
942
- * Generate + copy the token (you only see it once)
943
-
944
- ```bash
945
- serverrf --gist --set --id <gist_id> --file <filename>
946
- ```
947
-
948
- Run the server normally, and you should see your gist change every minute!
949
-
950
- ```bash
951
- serverrf -s
952
- ```
953
-
954
923
  <!-- todo: -->
955
924
  <!-- Adding plutos via the CLI -->
956
925
  <!-- Testing functionality (disregarding Host pipeline) -->
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "remoteRF-server"
7
- version = "1.7.12"
7
+ version = "1.7.13"
8
8
  description = "RemoteRF server-side control package"
9
9
  license = { text = "GPL-3.0-or-later" }
10
10
  requires-python = ">=3.10"
@@ -47,7 +47,7 @@ TIERS = {
47
47
  "help": READ, "status": READ, "devices list": READ, "users list": READ, "users perms": READ,
48
48
  "reservations list": READ, "groups list": READ, "codes list": READ, "hosts status": READ,
49
49
  "hosts tokens": READ, "certs list": READ, "config list": READ, "global status": READ,
50
- "cluster list": READ, "cluster info": READ, "gist list": READ, "users prune-proxies": READ,
50
+ "cluster list": READ, "cluster info": READ, "users prune-proxies": READ,
51
51
  "cluster codes list": READ, "schema browse": READ,
52
52
  # write
53
53
  "devices add": WRITE, "devices remove": WRITE, "devices rename": WRITE, "groups create": WRITE,
@@ -63,7 +63,7 @@ TIERS = {
63
63
  "groups remove": DESTRUCTIVE, "devices wipe": DESTRUCTIVE, "reservations purge": DESTRUCTIVE,
64
64
  "stats purge": DESTRUCTIVE, "hosts wipe": DESTRUCTIVE, "hosts forget": DESTRUCTIVE,
65
65
  "certs reset": DESTRUCTIVE, "certs wipe": DESTRUCTIVE, "config ports": DESTRUCTIVE,
66
- "cluster remove": DESTRUCTIVE, "gist set": DESTRUCTIVE, "gist wipe": DESTRUCTIVE,
66
+ "cluster remove": DESTRUCTIVE,
67
67
  # never remote: they cut remote access, nuke everything, or pin admins
68
68
  "stop": NEVER, "detach": NEVER, "clear": NEVER, "wipe": NEVER, "users purge": NEVER, "db purge": NEVER,
69
69
  "global cluster": NEVER, # an old alias for every `cluster` verb, remove included: use `cluster …`
@@ -74,7 +74,7 @@ TIERS = {
74
74
  }
75
75
  DENY_FLAGS = {"--secrets"}
76
76
  # Destructive commands whose own confirmation -y answers (the step-up asked already).
77
- TAKES_YES = {"devices wipe", "stats purge", "hosts wipe", "certs wipe", "gist wipe", "users prune-proxies",
77
+ TAKES_YES = {"devices wipe", "stats purge", "hosts wipe", "certs wipe", "users prune-proxies",
78
78
  "hosts tokens", "certs reset"}
79
79
  KEEP_BACKUPS = 10
80
80
  PAIR_CODE = re.compile(r"[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}-[A-Z2-7]{4}")
@@ -178,10 +178,6 @@ def _table() -> list[Command]:
178
178
  Command(("cluster peers",), "Partner listeners this deployment dials directly: cluster peers [add <id> <host:port> --ca <ca.crt> | remove <id>]", _federation("peers"), "Clusters"),
179
179
  Command(("cluster accept",), "", _federation("accept"), "Clusters", hidden=True),
180
180
  Command(("global cluster",), "", _federation("cluster"), "Global", hidden=True),
181
- # ---- gist ----
182
- Command(("gist list",), "Show gist status settings [--secrets]", _shell("cmd_gist_show"), "Gist status"),
183
- Command(("gist set",), "gist set <gist_id> <file> [--token-stdin]", _shell("cmd_gist_set"), "Gist status"),
184
- Command(("gist wipe",), "Forget the gist settings [-y]", _shell("cmd_gist_wipe"), "Gist status"),
185
181
  # ---- database ----
186
182
  Command(("db export",), "Archive DBs, devices, keys, certs to ~/Downloads [dir]", _shell("cmd_db_export"), "Database"),
187
183
  Command(("stats purge",), "Delete the activity log (reservation stats) [-y]", _daemon("cmd_stats_purge"), "Database", where=D),
@@ -244,7 +240,6 @@ _SECTION_BLURB = {
244
240
  "Global": "public address and relay",
245
241
  "Clusters": "sharing devices with other deployments",
246
242
  "Schemas": "driver schemas approved on RemoteRF Global",
247
- "Gist status": "publishing status to a GitHub gist",
248
243
  "Database": "export, or wipe everything",
249
244
  "Remote admin": "admins who reach this console from anywhere",
250
245
  }
@@ -62,11 +62,14 @@ Once an hour it sends Global a short note: this deployment's name and
62
62
  ID, version, uptime, how many accounts (and how many visit from partner
63
63
  deployments), what devices it has, how many reservations are active, how many
64
64
  reservations have been made and the total time reserved so far (two numbers),
65
- and your LAN address. Nothing else leaves your server -- no credentials, no
66
- user or reservation details -- and the connection only goes outward.
67
- Global publishes the network totals, and each deployment's name, version,
68
- devices, when it joined and was last online, and how many reservations it
69
- has had and the time reserved, at https://{domain}/stats.json.
65
+ and your LAN address. For the devices you share on Global it also sends how
66
+ many reservations start in each hour of the day and on each day of the week
67
+ (UTC), and the reservations and time reserved per device type (counts only).
68
+ Nothing else leaves your server -- no credentials, no user or reservation
69
+ details -- and the connection only goes outward. Global publishes the network
70
+ totals, these breakdowns, and each deployment's name, version, devices, when
71
+ it joined and was last online, and how many reservations it has had and the
72
+ time reserved, at https://{domain}/stats.json.
70
73
 
71
74
  A RemoteRF admin will review your new deployment before its address goes live.
72
75
  If you opt out, you cannot opt back in without a full data reset. You may leave
@@ -737,6 +740,7 @@ STATUS_FIELDS = frozenset(
737
740
  "active_reservations",
738
741
  "reservations",
739
742
  "reserved_s",
743
+ "usage",
740
744
  "lan_address",
741
745
  "cert_sha256",
742
746
  }
@@ -758,23 +762,24 @@ def _db_dir() -> Path:
758
762
  return cfg_dir() / "db"
759
763
 
760
764
 
761
- def _count(database: Path, sql: str, params=()) -> int:
765
+ def _rows(database: Path, sql: str, params=()) -> list:
762
766
  import sqlite3
763
767
 
764
768
  if not database.exists():
765
- return 0
769
+ return []
766
770
  try:
767
771
  with sqlite3.connect(f"file:{database}?mode=ro", uri=True, timeout=2.0) as db:
768
- return int(db.execute(sql, params).fetchone()[0])
772
+ return db.execute(sql, params).fetchall()
769
773
  except sqlite3.Error:
770
- return 0
774
+ return []
771
775
 
772
776
 
773
- def device_summary() -> list:
774
- """One entry per registered device: its type and whether it is connected.
777
+ def _count(database: Path, sql: str, params=()) -> int:
778
+ rows = _rows(database, sql, params)
779
+ return int(rows[0][0]) if rows else 0
775
780
 
776
- Types and counts only — never a device identifier, serial, or any data.
777
- """
781
+
782
+ def _registered_devices() -> list:
778
783
  import yaml
779
784
 
780
785
  path = cfg_dir() / "devices.yml"
@@ -783,23 +788,74 @@ def device_summary() -> list:
783
788
  devices = data.get("devices") or []
784
789
  except (OSError, yaml.YAMLError):
785
790
  return []
791
+ return [item for item in devices if type(item) is dict and "device_id" in item]
792
+
793
+
794
+ def _device_type(item: dict) -> str:
795
+ return str(item.get("device_type") or "unknown").lower()
796
+
797
+
798
+ def _device_types() -> dict:
799
+ """Device id -> type: devices.yml entries, then the Host directory `serverrf status` lists.
800
+
801
+ Types are bounded the way Global accepts them: a Host names its own kinds.
802
+ """
803
+ types = {int(item["device_id"]): _device_type(item) for item in _registered_devices()}
804
+ try:
805
+ from ..host import host_tunnel_server as hts
806
+
807
+ directory = hts.get_tunnel_registry().device_directory_cached(ttl_ms=0)
808
+ except Exception: # noqa: BLE001 - drivers are optional at report time
809
+ directory = {}
810
+ for device_id, (_host_id, info, _active) in directory.items():
811
+ if str(device_id).isdigit():
812
+ types.setdefault(int(device_id), str(getattr(info, "kind", "") or "").strip().lower() or "unknown")
813
+ return {gid: re.sub(r"[^a-z0-9_]", "_", kind)[:64] for gid, kind in types.items()}
814
+
815
+
816
+ def device_summary() -> list:
817
+ """One entry per registered device: its type and whether it is connected.
818
+
819
+ Types and counts only — never a device identifier, serial, or any data.
820
+ """
786
821
  try:
787
822
  from . import device_manager
788
823
 
789
824
  connected = set(device_manager.get_all_devices())
790
825
  except Exception: # noqa: BLE001 - drivers are optional at report time
791
826
  connected = set()
792
- summary = []
793
- for item in devices:
794
- if type(item) is not dict or "device_id" not in item:
827
+ return [{"type": kind, "online": gid in connected} for gid, kind in _device_types().items()]
828
+
829
+
830
+ def usage_summary() -> dict:
831
+ """Reservations made on the devices shared on Global: by the UTC hour and
832
+ weekday they start, and reservations and seconds reserved per device type.
833
+
834
+ Counts only (ReservationBuckets); all zero when nothing is shared there.
835
+ """
836
+ from .federation import cluster
837
+
838
+ usage = {"by_hour": [0] * 24, "by_weekday": [0] * 7, "by_type": {}}
839
+ try:
840
+ shared = {int(d) for group in cluster.groups(cluster.GLOBAL) for d in group["devices"]}
841
+ except Exception: # noqa: BLE001 - not in the global cluster (yet), or its state is unreadable
842
+ return usage
843
+ types = _device_types()
844
+ by_type = {}
845
+ for device_id, bucket, n, seconds in _rows(
846
+ _db_dir() / "reservations.db",
847
+ "SELECT device_id, bucket, reservations, CAST(ROUND(seconds) AS INTEGER) FROM ReservationBuckets"):
848
+ if device_id not in shared:
795
849
  continue
796
- summary.append(
797
- {
798
- "type": str(item.get("device_type") or "unknown").lower(),
799
- "online": int(item["device_id"]) in connected,
800
- }
801
- )
802
- return summary
850
+ if bucket == "all":
851
+ totals = by_type.setdefault(types.get(device_id, "unknown"), {"reservations": 0, "seconds": 0})
852
+ totals["reservations"] += n
853
+ totals["seconds"] = min(totals["seconds"] + seconds, 2**31)
854
+ else:
855
+ usage["by_hour" if bucket[0] == "h" else "by_weekday"][int(bucket[1:])] += n
856
+ busiest = sorted((kv for kv in by_type.items() if kv[1]["reservations"]), key=lambda kv: -kv[1]["reservations"])
857
+ usage["by_type"] = dict(busiest[:64]) # Global takes at most 64 types
858
+ return usage
803
859
 
804
860
 
805
861
  def cert_sha256() -> Optional[str]:
@@ -832,6 +888,7 @@ def status_payload() -> dict:
832
888
  ),
833
889
  "reservations": _count(reservations, "SELECT n FROM ReservationCount"),
834
890
  "reserved_s": _count(reservations, "SELECT CAST(ROUND(seconds) AS INTEGER) FROM ReservedTotal"),
891
+ "usage": usage_summary(),
835
892
  "lan_address": lan_endpoint(),
836
893
  "cert_sha256": cert_sha256(),
837
894
  }
@@ -851,8 +908,9 @@ def send_status() -> None:
851
908
 
852
909
  payload = status_payload()
853
910
  # A Global older than a field refuses the note: drop the newest first
854
- # (`reservations`), then `proxies`/`reserved_s`, as the note was before them.
855
- steps = ((), ("reservations",), ("reservations", "proxies", "reserved_s"))
911
+ # (`usage`, then `reservations`), then `proxies`/`reserved_s`, as the note
912
+ # was before each of them.
913
+ steps = ((), ("usage",), ("usage", "reservations"), ("usage", "reservations", "proxies", "reserved_s"))
856
914
  for drop in steps:
857
915
  try:
858
916
  call("POST", REPORT_PATH, {"kind": "status", "status": {k: v for k, v in payload.items() if k not in drop}})
@@ -69,7 +69,6 @@ from prompt_toolkit import PromptSession
69
69
  from prompt_toolkit.input import DummyInput
70
70
  from prompt_toolkit.output import DummyOutput
71
71
 
72
- from ..tools.gist_status import start_status_publisher
73
72
  from .cert_provider import start_cert_provider, stop_cert_provider
74
73
  try:
75
74
  from . import direct_path
@@ -296,7 +295,6 @@ def start_server_runtime(
296
295
  cert_dir: Path | None = None,
297
296
  include_host_tunnel: bool = True,
298
297
  run_reservation_updates: bool = True,
299
- publish_status: bool = True,
300
298
  federation_executor: Any = None,
301
299
  home_federation_enroller: Any = None,
302
300
  home_federation_permissions: Any = None,
@@ -444,8 +442,6 @@ def start_server_runtime(
444
442
  )
445
443
  update_thread.start()
446
444
  start_usergroup_cleanup_loop(interval_sec=60)
447
- if publish_status:
448
- start_status_publisher()
449
445
  except BaseException: # Ctrl-C during the startup check too: close_local_devices powers Plutos down
450
446
  stop_event.set()
451
447
  server.stop(0)
@@ -252,6 +252,46 @@ class ReservationHandler:
252
252
  END;
253
253
  ''')
254
254
 
255
+ # The same two counts per own device: overall ('all'), by the UTC hour
256
+ # of the start ('h00'..'h23') and by its UTC weekday ('d0' = Monday ..
257
+ # 'd6'). Reservations hold local time, hence the 'utc' modifier.
258
+ # Reported to Global as `usage`, for the devices shared there. Seeded
259
+ # only into an empty table: the bucket names follow the process TZ, so a
260
+ # restart under another TZ must not count the current rows again.
261
+ hour = lambda r: f"'h' || strftime('%H', {r}.start_time, 'utc')" # noqa: E731
262
+ day = lambda r: f"'d' || ((CAST(strftime('%w', {r}.start_time, 'utc') AS INTEGER) + 6) % 7)" # noqa: E731
263
+ length = lambda r: f"(julianday({r}.end_time) - julianday({r}.start_time)) * 86400" # noqa: E731
264
+ self.cursor.executescript(f'''
265
+ BEGIN IMMEDIATE;
266
+ CREATE TABLE IF NOT EXISTS ReservationBuckets (device_id INTEGER NOT NULL, bucket TEXT NOT NULL,
267
+ reservations INTEGER NOT NULL, seconds REAL NOT NULL, PRIMARY KEY (device_id, bucket));
268
+ INSERT INTO ReservationBuckets
269
+ SELECT device_id, bucket, COUNT(*), SUM(seconds) FROM (
270
+ SELECT device_id, 'all' AS bucket, {length("r")} AS seconds FROM Reservations r
271
+ UNION ALL SELECT device_id, {hour("r")}, {length("r")} FROM Reservations r
272
+ UNION ALL SELECT device_id, {day("r")}, {length("r")} FROM Reservations r)
273
+ WHERE device_id < 1000000 AND NOT EXISTS (SELECT 1 FROM ReservationBuckets)
274
+ GROUP BY device_id, bucket;
275
+ CREATE TRIGGER IF NOT EXISTS reservation_buckets_ins AFTER INSERT ON Reservations
276
+ WHEN NEW.device_id < 1000000 BEGIN
277
+ INSERT INTO ReservationBuckets VALUES
278
+ (NEW.device_id, 'all', 1, {length("NEW")}),
279
+ (NEW.device_id, {hour("NEW")}, 1, {length("NEW")}),
280
+ (NEW.device_id, {day("NEW")}, 1, {length("NEW")})
281
+ ON CONFLICT (device_id, bucket) DO UPDATE
282
+ SET reservations = reservations + 1, seconds = seconds + excluded.seconds;
283
+ END;
284
+ CREATE TRIGGER IF NOT EXISTS reservation_buckets_del AFTER DELETE ON Reservations
285
+ WHEN OLD.device_id < 1000000 BEGIN
286
+ UPDATE ReservationBuckets SET
287
+ reservations = MAX(reservations - (OLD.start_time > datetime('now', 'localtime')), 0),
288
+ seconds = MAX(seconds - MAX(0,
289
+ (julianday(OLD.end_time) - julianday(MAX(OLD.start_time, datetime('now', 'localtime')))) * 86400), 0)
290
+ WHERE device_id = OLD.device_id AND bucket IN ('all', {hour("OLD")}, {day("OLD")});
291
+ END;
292
+ COMMIT;
293
+ ''')
294
+
255
295
  # "Stay logged in" for the deployment protocol: a random token the
256
296
  # client keeps instead of the password. Only its hash lives here, so
257
297
  # the table reveals nothing; a row is revoked by logout or purge.
@@ -1454,172 +1494,6 @@ class ReservationHandler:
1454
1494
  except (sqlite3.Error, ValueError):
1455
1495
  return {}
1456
1496
 
1457
- @db_connection
1458
- def get_obfuscated_device_reservations_grouped_past_days(self, days: int) -> dict[str, dict[str, int]]:
1459
- try:
1460
- days = int(days)
1461
- except Exception:
1462
- return {}
1463
-
1464
- if days < 0:
1465
- return {}
1466
-
1467
- now = datetime.now()
1468
- cutoff = now - timedelta(days=days)
1469
-
1470
- def _parse_logged_int(v) -> int:
1471
- if isinstance(v, int):
1472
- return v
1473
- if isinstance(v, float):
1474
- return int(v)
1475
-
1476
- s = str(v).strip()
1477
-
1478
- # Handles strings like:
1479
- # "int64_value: 2"
1480
- # "int64_value: 1772587800"
1481
- if ":" in s:
1482
- s = s.split(":", 1)[1].strip()
1483
-
1484
- # remove quotes/newlines if present
1485
- s = s.strip().strip('"').strip("'")
1486
- return int(float(s))
1487
-
1488
- # Take a consistent snapshot of the log file under the same lock used by writers.
1489
- try:
1490
- with log_lock:
1491
- activity_log = _load_logs()
1492
- except Exception:
1493
- activity_log = []
1494
-
1495
- out: dict[str, dict[str, int]] = {}
1496
-
1497
- for entry in activity_log:
1498
- try:
1499
- if not isinstance(entry, dict):
1500
- continue
1501
-
1502
- if str(entry.get("action", "")).strip() != "reserve_device":
1503
- continue
1504
-
1505
- details = entry.get("details", {})
1506
- if not isinstance(details, dict):
1507
- continue
1508
-
1509
- args = details.get("args", {})
1510
- result = details.get("result", {})
1511
-
1512
- if not isinstance(args, dict) or not isinstance(result, dict):
1513
- continue
1514
-
1515
- # Only count SUCCESSFUL reservations
1516
- if "Token" not in result:
1517
- continue
1518
-
1519
- raw_device_id = args.get("dd")
1520
- raw_start = args.get("st")
1521
- raw_end = args.get("et")
1522
-
1523
- if raw_device_id is None or raw_start is None or raw_end is None:
1524
- continue
1525
-
1526
- device_id = _parse_logged_int(raw_device_id)
1527
- start_ts = _parse_logged_int(raw_start)
1528
- end_ts = _parse_logged_int(raw_end)
1529
-
1530
- start_dt = datetime.fromtimestamp(start_ts)
1531
- end_dt = datetime.fromtimestamp(end_ts)
1532
-
1533
- # Same overlap semantics as before
1534
- if end_dt < cutoff or start_dt > now:
1535
- continue
1536
-
1537
- dev_key = str(device_id)
1538
- day_key = start_dt.date().isoformat()
1539
-
1540
- if dev_key not in out:
1541
- out[dev_key] = {}
1542
-
1543
- out[dev_key][day_key] = out[dev_key].get(day_key, 0) + 1
1544
-
1545
- except Exception:
1546
- continue
1547
-
1548
- return dict(sorted(out.items(), key=lambda kv: int(kv[0])))
1549
-
1550
- @db_connection
1551
- def get_obfuscated_usage_summary(self) -> dict[str, int | float]:
1552
-
1553
- int_re = re.compile(r'int64_value:\s*([0-9]+)')
1554
- str_re = re.compile(r'string_value:\s*"([^"]*)"')
1555
-
1556
- def _parse_proto_value(v):
1557
- if isinstance(v, (int, float)):
1558
- return v
1559
- if not isinstance(v, str):
1560
- return v
1561
-
1562
- m_int = int_re.search(v)
1563
- if m_int:
1564
- return int(m_int.group(1))
1565
-
1566
- m_str = str_re.search(v)
1567
- if m_str:
1568
- return m_str.group(1)
1569
-
1570
- return v
1571
-
1572
- try:
1573
- with log_lock:
1574
- activity_log = _load_logs()
1575
- except Exception:
1576
- activity_log = []
1577
-
1578
- accounts: set[str] = set()
1579
- total_reservations = 0
1580
- total_reserved_seconds = 0
1581
-
1582
- for entry in activity_log:
1583
- try:
1584
- if not isinstance(entry, dict):
1585
- continue
1586
-
1587
- action = entry.get("action")
1588
- username = entry.get("username")
1589
- details = entry.get("details", {}) or {}
1590
- args = details.get("args", {}) or {}
1591
- result = details.get("result", {}) or {}
1592
-
1593
- if not isinstance(args, dict) or not isinstance(result, dict):
1594
- continue
1595
-
1596
- # Count accounts from successful logins
1597
- if action == "login" and "UC" in result:
1598
- uc_name = _parse_proto_value(result.get("UC"))
1599
- if uc_name:
1600
- accounts.add(str(uc_name))
1601
- elif username:
1602
- accounts.add(str(username))
1603
-
1604
- # Count reservations + reserved time
1605
- if action == "reserve_device":
1606
- total_reservations += 1
1607
-
1608
- st = _parse_proto_value(args.get("st"))
1609
- et = _parse_proto_value(args.get("et"))
1610
-
1611
- if isinstance(st, int) and isinstance(et, int) and et > st:
1612
- total_reserved_seconds += (et - st)
1613
-
1614
- except Exception:
1615
- continue
1616
-
1617
- return {
1618
- "account_count": len(accounts),
1619
- "total_reservations": int(total_reservations),
1620
- "total_reserved_hours": round(total_reserved_seconds / 3600.0, 1),
1621
- }
1622
-
1623
1497
  def get_reservation_count(self, username):
1624
1498
  self.cursor.execute("SELECT COUNT(*) FROM Reservations WHERE username = ?", (username,))
1625
1499
  count = self.cursor.fetchone()[0]