shareddata 6.85.0__tar.gz → 6.85.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. {shareddata-6.85.0/src/shareddata.egg-info → shareddata-6.85.1}/PKG-INFO +1 -1
  2. {shareddata-6.85.0 → shareddata-6.85.1}/setup.py +1 -1
  3. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/apidocs.yml +1 -1
  4. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/resolve.py +11 -3
  5. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Redaction.py +29 -0
  6. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Table.py +55 -3
  7. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/config.py +1 -0
  8. shareddata-6.85.1/src/SharedData/Web/proc.py +170 -0
  9. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/standalone.py +31 -1
  10. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/standalone.css +13 -0
  11. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/standalone.js +132 -0
  12. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/standalone.html +7 -0
  13. {shareddata-6.85.0 → shareddata-6.85.1/src/shareddata.egg-info}/PKG-INFO +1 -1
  14. {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/SOURCES.txt +1 -0
  15. {shareddata-6.85.0 → shareddata-6.85.1}/LICENSE +0 -0
  16. {shareddata-6.85.0 → shareddata-6.85.1}/MANIFEST.in +0 -0
  17. {shareddata-6.85.0 → shareddata-6.85.1}/README.md +0 -0
  18. {shareddata-6.85.0 → shareddata-6.85.1}/pyproject.toml +0 -0
  19. {shareddata-6.85.0 → shareddata-6.85.1}/setup.cfg +0 -0
  20. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/ServerGunicorn.py +0 -0
  21. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/ServerWaitress.py +0 -0
  22. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/__init__.py +0 -0
  23. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/apidocs-dark.css +0 -0
  24. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/auth.py +0 -0
  25. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/constants.py +0 -0
  26. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/download_paths.py +0 -0
  27. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/download_slots.py +0 -0
  28. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/install/worker.ps1 +0 -0
  29. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/install/worker.sh +0 -0
  30. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/listcache.py +0 -0
  31. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/__init__.py +0 -0
  32. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/cache.py +0 -0
  33. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/collections.py +0 -0
  34. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/data.py +0 -0
  35. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/downloads.py +0 -0
  36. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/metadata.py +0 -0
  37. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/operations.py +0 -0
  38. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/schedules.py +0 -0
  39. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/sqlite.py +0 -0
  40. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/system.py +0 -0
  41. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/tables.py +0 -0
  42. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/timeseries.py +0 -0
  43. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/web_routes.py +0 -0
  44. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/routes/workers.py +0 -0
  45. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/API/utils.py +0 -0
  46. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/__init__.py +0 -0
  47. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/bootstrap.py +0 -0
  48. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/migrate.py +0 -0
  49. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/store.py +0 -0
  50. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Auth/workers.py +0 -0
  51. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/CacheRedis.py +0 -0
  52. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/CollectionMongoDB.py +0 -0
  53. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/__init__.py +0 -0
  54. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/catalog.py +0 -0
  55. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/config.py +0 -0
  56. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/errors.py +0 -0
  57. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/indexes.py +0 -0
  58. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/readers.py +0 -0
  59. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/DataAPI/service.py +0 -0
  60. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Database.py +0 -0
  61. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Defaults.py +0 -0
  62. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AWSEC2.py +0 -0
  63. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AWSS3.py +0 -0
  64. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/AutoDocstrings.py +0 -0
  65. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientAPI.py +0 -0
  66. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientSocket.py +0 -0
  67. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ClientWebSocket.py +0 -0
  68. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  69. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/LogReader.py +0 -0
  70. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/MongoDBClient.py +0 -0
  71. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/SaveTables.py +0 -0
  72. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ServerSocket.py +0 -0
  73. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/ServerWebSocket.py +0 -0
  74. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/StreamsCache.py +0 -0
  75. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/StreamsPersist.py +0 -0
  76. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/SyncTable.py +0 -0
  77. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  78. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/IO/__init__.py +0 -0
  79. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Listing.py +0 -0
  80. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/LockWaiters.py +0 -0
  81. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Logger.py +0 -0
  82. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/__init__.py +0 -0
  83. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/__main__.py +0 -0
  84. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/client.py +0 -0
  85. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MCP/server.py +0 -0
  86. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Metadata.py +0 -0
  87. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/MultiProc.py +0 -0
  88. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/OpenFIGI.py +0 -0
  89. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  90. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/BatchJob.py +0 -0
  91. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ContainerHealth.py +0 -0
  92. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/JobStore.py +0 -0
  93. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/RoutineCommand.py +0 -0
  94. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  95. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Schedule.py +0 -0
  96. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  97. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  98. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/ScheduleStore.py +0 -0
  99. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Scheduler.py +0 -0
  100. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  101. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/Worker.py +0 -0
  102. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  103. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerLib.py +0 -0
  104. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/WorkerPool.py +0 -0
  105. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Routines/__init__.py +0 -0
  106. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SQLite.py +0 -0
  107. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SharedData.py +0 -0
  108. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/SharedNumpy.py +0 -0
  109. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/StreamKafka.py +0 -0
  110. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Symbol.py +0 -0
  111. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableDisk.py +0 -0
  112. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndex.py +0 -0
  113. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitFunctions.py +0 -0
  114. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  115. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitGenerate.py +0 -0
  116. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitHash.py +0 -0
  117. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableIndexJitLoc.py +0 -0
  118. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableProtocol.py +0 -0
  119. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableRead.py +0 -0
  120. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TableRewrite.py +0 -0
  121. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TimeSeriesDisk.py +0 -0
  122. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/TimeseriesContainer.py +0 -0
  123. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Users.py +0 -0
  124. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Utils.py +0 -0
  125. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  126. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/__init__.py +0 -0
  127. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/activity_logger.py +0 -0
  128. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/app.py +0 -0
  129. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  130. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  131. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/google_logo.png +0 -0
  132. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  133. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/logs.css +0 -0
  134. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/sched.css +0 -0
  135. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/assets/styles.css +0 -0
  136. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/auth.py +0 -0
  137. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/backends.py +0 -0
  138. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/containers.py +0 -0
  139. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/identity.py +0 -0
  140. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/kafka_view.py +0 -0
  141. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/mongo_view.py +0 -0
  142. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/redis_view.py +0 -0
  143. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/route_health.py +0 -0
  144. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/route_registry.py +0 -0
  145. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/sched.py +0 -0
  146. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/schedule_editor.py +0 -0
  147. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/server.py +0 -0
  148. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/services.py +0 -0
  149. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/shell.py +0 -0
  150. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/account.js +0 -0
  151. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/custom.css +0 -0
  152. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/favicon.ico +0 -0
  153. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/favicon.png +0 -0
  154. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/logo.png +0 -0
  155. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/logo_96x96.png +0 -0
  156. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  157. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shell.css +0 -0
  158. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/static/shell.js +0 -0
  159. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/suggest.py +0 -0
  160. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/account.html +0 -0
  161. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/base.html +0 -0
  162. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/claim.html +0 -0
  163. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/claim_done.html +0 -0
  164. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/dash.html +0 -0
  165. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/index.html +0 -0
  166. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/login.html +0 -0
  167. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/login_2fa.html +0 -0
  168. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  169. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  170. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  171. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/templates/shell.html +0 -0
  172. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/twofactor.py +0 -0
  173. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/utils.py +0 -0
  174. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/Web/workerpool.py +0 -0
  175. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/__init__.py +0 -0
  176. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/build_info.py +0 -0
  177. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/fleet.py +0 -0
  178. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/preflight.py +0 -0
  179. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/private_files.py +0 -0
  180. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/release_backup.py +0 -0
  181. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/server.py +0 -0
  182. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/services.py +0 -0
  183. {shareddata-6.85.0 → shareddata-6.85.1}/src/SharedData/sharedmutexwin.pyd +0 -0
  184. {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/dependency_links.txt +0 -0
  185. {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/entry_points.txt +0 -0
  186. {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/requires.txt +0 -0
  187. {shareddata-6.85.0 → shareddata-6.85.1}/src/shareddata.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.0
3
+ Version: 6.85.1
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
@@ -66,7 +66,7 @@ test_requires = [
66
66
 
67
67
  setup(
68
68
  name='shareddata',
69
- version='6.85.0',
69
+ version='6.85.1',
70
70
  description='Memory Mapped / Shared Memory Database with S3 repository',
71
71
  author='shareddata',
72
72
  author_email='admin@shareddata.net',
@@ -2,7 +2,7 @@
2
2
  openapi: 3.0.3
3
3
  info:
4
4
  title: SharedData API
5
- version: 6.85.0
5
+ version: 6.85.1
6
6
  description: |
7
7
  HTTP API of a SharedData server. The same operations are available from Python
8
8
  through `SharedData.IO.ClientAPI` (for example `ClientAPI.get_table(...)`).
@@ -164,16 +164,24 @@ class SymbolRegistry:
164
164
 
165
165
  def identify(self, query, remote=True):
166
166
  """Identity of *query*: a registry symbol, a ticker or vendor alias of exactly one
167
- registry symbol, or a provider symbol of the registry collection (*remote*)."""
167
+ registry symbol, or a provider symbol of the registry collection (*remote*).
168
+ Active alias matches take precedence across fields; field order breaks ties.
169
+ Exact registry symbols and aliases with no active match retain their behavior."""
168
170
  query = str(query or '').strip()
169
171
  if not query:
170
172
  raise BadRequest('a symbol is required')
171
173
  self._load()
172
174
  if query in self._rows:
173
175
  return self._known(query, query, self._facts(self._rows[query]), 'metadata')
176
+ matches = [(name, lookup.get(query, [])) for name, lookup in self._reverse.items()]
177
+ if self._records is not None and 'isactive' in self._records.dtype.names:
178
+ active = {position for _, positions in matches for position in positions
179
+ if self._records['isactive'][position] == 1}
180
+ if active:
181
+ matches = [(name, [position for position in positions if position in active])
182
+ for name, positions in matches]
174
183
  candidates = []
175
- for name, lookup in self._reverse.items():
176
- positions = lookup.get(query, [])
184
+ for name, positions in matches:
177
185
  if len(positions) == 1:
178
186
  return self._known(query, self._symbol(positions[0]), self._facts(positions[0]), f'alias:{name}')
179
187
  candidates.extend(self._symbol(position) for position in positions)
@@ -912,9 +912,38 @@ class RedactingStream:
912
912
  return getattr(self.stream, name)
913
913
 
914
914
 
915
+ class _ChildDebugFilter(logging.Filter):
916
+ """Keep Flask's level-less default handler from printing propagated DEBUG records."""
917
+ def __init__(self):
918
+ super().__init__()
919
+ self.names = set()
920
+
921
+ def filter(self, record):
922
+ return record.levelno > logging.DEBUG or record.name in self.names
923
+
924
+
925
+ def _quiet_flask_default_handler(logger):
926
+ """Reading ``app.logger`` makes Flask attach its default handler, which has no
927
+ level, to a logger named after the app (``SharedData.API``). Every child logger
928
+ under that dotted name (the server's own, set to DEBUG with an INFO console
929
+ handler) propagates into it, so each ``#heartbeat#`` debug record was printed
930
+ again in Flask's format. Only the app logger's own DEBUG records stay visible;
931
+ INFO and above from children are untouched.
932
+ """
933
+ from flask.logging import default_handler
934
+ if default_handler not in logger.handlers:
935
+ return
936
+ quiet = getattr(default_handler, '_shareddata_child_debug', None)
937
+ if quiet is None:
938
+ quiet = default_handler._shareddata_child_debug = _ChildDebugFilter()
939
+ default_handler.addFilter(quiet)
940
+ quiet.names.add(logger.name)
941
+
942
+
915
943
  def install_flask_redaction(app):
916
944
  """Protect Flask logging and its request-local wsgi.errors writer."""
917
945
  install_redaction(app.logger)
946
+ _quiet_flask_default_handler(app.logger)
918
947
  if not getattr(app, '_shareddata_redaction', False):
919
948
  original = app.wsgi_app
920
949
  def redacted_wsgi(environ, start_response):
@@ -17,6 +17,8 @@ import asyncio
17
17
  import importlib
18
18
  import bson
19
19
  import mmap
20
+ import stat
21
+ import uuid
20
22
 
21
23
  from SharedData.Logger import Logger
22
24
  from SharedData.LockWaiters import TableBusy
@@ -635,14 +637,18 @@ class Table:
635
637
  Raises:
636
638
  Exception: If the table does not exist and cannot be created, or if the index cannot be properly created.
637
639
  """
638
- if self.type == 1 and self.filepath.exists():
640
+ replace_image = self.type == 1 and self.filepath.exists()
641
+ if replace_image and not self.overwrite:
639
642
  from SharedData.TableRewrite import RewriteError
640
643
  raise RewriteError('overwriting an existing disk image requires staged publication')
641
644
  if (not self.names is None) \
642
645
  & (not self.formats is None)\
643
646
  & (not self.size is None):
644
- self.create_header()
645
- self.malloc(create=True)
647
+ self.create_header()
648
+ if replace_image:
649
+ self._publish_empty_image()
650
+ else:
651
+ self.malloc(create=True)
646
652
 
647
653
  elif (not self.exists) | (self.overwrite):
648
654
  if not self.exists:
@@ -656,6 +662,52 @@ class Table:
656
662
  if self.is_schemaless:
657
663
  self.init_bson_mmap()
658
664
 
665
+ def _publish_empty_image(self):
666
+ """
667
+ Replace the existing disk image with a new empty one (overwrite=True).
668
+
669
+ The new image is built complete under a stage name beside data.bin and
670
+ installed by one os.replace, inside the caller's STRUCT section, so a
671
+ failure or crash before the replace leaves the old image untouched.
672
+ The mappings made on the stage stay valid after the rename (same inode);
673
+ other processes reopen by path when the epoch is published on exit from
674
+ the structure section. The index and BSON files only describe the old
675
+ records, so they are removed and rebuilt from the new empty header.
676
+ """
677
+ from SharedData.TableRewrite import (RewriteError, fsync_directory,
678
+ require_rewrite_support)
679
+ require_rewrite_support()
680
+ final = self.filepath
681
+ old = os.stat(final)
682
+ # The writer lock is held: any earlier stage is from a build that died.
683
+ for leftover in final.parent.glob('data.bin.overwrite.*'):
684
+ leftover.unlink()
685
+ stage = final.parent / ('data.bin.overwrite.' + uuid.uuid4().hex)
686
+ self.filepath = stage
687
+ try:
688
+ self.malloc(create=True)
689
+ os.chmod(stage, stat.S_IMODE(old.st_mode))
690
+ try:
691
+ os.chown(stage, old.st_uid, old.st_gid)
692
+ except OSError:
693
+ pass
694
+ os.replace(stage, final)
695
+ except BaseException:
696
+ self.filepath = final
697
+ try:
698
+ stage.unlink()
699
+ except FileNotFoundError:
700
+ pass
701
+ raise
702
+ self.filepath = final
703
+ fsync_directory(final.parent)
704
+ for name in ('bson.bin', 'dateidx.bin', 'pkey.bin', 'symbolidx.bin', 'portidx.bin'):
705
+ try:
706
+ (final.parent / name).unlink()
707
+ except FileNotFoundError:
708
+ pass
709
+ fsync_directory(final.parent)
710
+
659
711
  def create_header(self):
660
712
 
661
713
  """
@@ -216,6 +216,7 @@ STANDALONE_COMMANDS = {
216
216
  'ROUTES': {'route': '/admin/routes', 'description': 'Other web services opened from this site', 'type': 'command', 'group': 'Admin', 'master': True},
217
217
  'WORKERS': {'route': '/admin/workers', 'description': 'Add workers, join and command tokens, Git credentials', 'type': 'command', 'group': 'Admin', 'master': True},
218
218
  'SCHED': {'route': '/admin/sched', 'description': 'Schedules: status of every routine, restart and stop', 'type': 'command', 'group': 'System', 'master': True},
219
+ 'PROC': {'route': '/admin/proc', 'description': 'Processes: what each routine running on this server uses, restart and stop', 'type': 'command', 'group': 'System', 'master': True},
219
220
  'LOGS': {'route': '/admin/logs', 'description': 'Live tail of the logs', 'type': 'command', 'group': 'System', 'master': True},
220
221
  'WORKERPOOL': {'route': '/admin/workerpool', 'description': 'Worker pool: a terminal to the workers, and the repositories they can run', 'type': 'command', 'group': 'System', 'master': True},
221
222
  # The services SharedData can use: listed whether or not one is configured, since each page is also where its connection is set.
@@ -0,0 +1,170 @@
1
+ """The PROC page's data: the routines whose processes run on this server, and the two commands
2
+ a master may send from it (restart, stop).
3
+
4
+ A routine is one line however many processes it has (a routine with several workers is one
5
+ card). Its numbers are those of all its processes together. Platform processes (the Worker, the
6
+ Scheduler, this server) are shown but are not restarted or stopped from here: their Worker keeps
7
+ them running, and stopping the server would stop the page. So are processes that are not a script
8
+ the Worker runs (an installed program such as vllm or rtxclaw): a restart would try to start that
9
+ file as a Python script.
10
+ """
11
+ import hashlib
12
+ import os
13
+ import threading
14
+ import time
15
+ from datetime import datetime, timezone
16
+
17
+ import psutil
18
+
19
+ from SharedData.Routines.RoutineCommand import is_scheduler
20
+ from SharedData.Web import sched
21
+
22
+ PIDS_SHOWN = 20 # process ids listed on a card
23
+ SAMPLE_FRESH = 30 # seconds a CPU reading can serve as the start of the next one
24
+ SAMPLE_WAIT = 0.25 # seconds the first reading of a process is taken over
25
+ BAD_STATES = ('zombie', 'dead', 'stopped', 'stop')
26
+ # What the page asks for -> the worker's job (its own 'stop' does nothing).
27
+ JOBS = {job: sched.JOBS[job] for job in ('restart', 'stop')}
28
+
29
+ _samples = {} # (pid, start) -> (monotonic seconds, cpu seconds), kept between two readings
30
+ _lock = threading.Lock()
31
+
32
+
33
+ def _script(command):
34
+ """What follows 'user@COMPUTER:' in a routine's name, as the SCHED page writes it."""
35
+ repo, routine = command.get('repo') or '', command.get('routine') or ''
36
+ if repo == 'SharedData':
37
+ return 'SharedData.' + routine
38
+ branch = command.get('branch')
39
+ return repo + ('#' + branch if branch and branch != 'nan' else '') + '/' + (routine[:-3] if routine.endswith('.py') else routine)
40
+
41
+
42
+ def _key(command):
43
+ parts = [str(command.get(name) or '') for name in ('repo', 'branch', 'routine', 'args')]
44
+ return hashlib.sha1('\0'.join(parts).encode()).hexdigest()[:12]
45
+
46
+
47
+ def _cpu_seconds(process):
48
+ times = process.cpu_times()
49
+ return times.user + times.system
50
+
51
+
52
+ def _read_cpu(processes):
53
+ """{(pid, start): percent of one core since the previous reading of that process}. A process
54
+ without a recent reading is measured over one short wait, shared by all of them; the page
55
+ asks every few seconds, so the later readings do not wait."""
56
+ first = time.monotonic()
57
+ by_key, spent = {}, {}
58
+ for process in processes:
59
+ try:
60
+ key = (process.pid, process.create_time())
61
+ by_key[key], spent[key] = process, _cpu_seconds(process)
62
+ except psutil.Error:
63
+ continue
64
+ with _lock:
65
+ known = {key: _samples[key] for key in by_key if key in _samples and first - _samples[key][0] <= SAMPLE_FRESH}
66
+ percent = {key: max(0.0, (spent[key] - used) / max(first - when, 1e-3) * 100) for key, (when, used) in known.items()}
67
+ new = [key for key in by_key if key not in known]
68
+ if new:
69
+ time.sleep(SAMPLE_WAIT)
70
+ later = time.monotonic()
71
+ for key in new:
72
+ try:
73
+ percent[key] = max(0.0, (_cpu_seconds(by_key[key]) - spent[key]) / max(later - first, 1e-3) * 100)
74
+ except psutil.Error:
75
+ percent[key] = 0.0
76
+ with _lock:
77
+ for key in [key for key in _samples if key not in by_key]:
78
+ del _samples[key]
79
+ for key in by_key:
80
+ _samples[key] = (first, spent[key])
81
+ return percent
82
+
83
+
84
+ def _iso(epoch):
85
+ return datetime.fromtimestamp(epoch, timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
86
+
87
+
88
+ def _routines():
89
+ from SharedData.Routines.WorkerLib import update_routines
90
+ found = []
91
+ update_routines(found)
92
+ return found
93
+
94
+
95
+ def rows():
96
+ """The routines running here, one dict each. '_args' and '_branch' are the real values and
97
+ never leave this module (an argument can be a secret)."""
98
+ found = _routines()
99
+ processes = [routine['process'] for routine in found if routine.get('process') is not None]
100
+ cpu = _read_cpu(processes)
101
+ grouped = {}
102
+ for routine in found:
103
+ process, command = routine.get('process'), routine.get('command') or {}
104
+ try:
105
+ with process.oneshot():
106
+ start = process.create_time()
107
+ item = dict(pid=routine['pid'], cpu=cpu.get((process.pid, start), 0.0), memory=process.memory_info().rss,
108
+ threads=process.num_threads(), state=process.status(), started=start)
109
+ try:
110
+ item['files'] = process.num_fds() if hasattr(process, 'num_fds') else len(process.open_files())
111
+ except psutil.Error:
112
+ item['files'] = None
113
+ except Exception: # the process ended while it was being read
114
+ continue
115
+ key = _key(command)
116
+ row = grouped.setdefault(key, dict(
117
+ key=key, script=_script(command), repo=command.get('repo') or '', branch=command.get('branch') or '',
118
+ routine=command.get('routine') or '', args=sched.mask_args(command.get('args')), _args=command.get('args') or '',
119
+ processes=0, pids=[], cpu=0.0, memory_mb=0.0, threads=0, files=0, states=set(), started=None,
120
+ platform=command.get('repo') == 'SharedData', script_run=str(command.get('routine') or '').endswith('.py')))
121
+ row['processes'] += 1
122
+ row['pids'].append(item['pid'])
123
+ row['cpu'] += item['cpu']
124
+ row['memory_mb'] += item['memory'] / (1 << 20)
125
+ row['threads'] += item['threads']
126
+ row['files'] = None if row['files'] is None or item['files'] is None else row['files'] + item['files']
127
+ row['states'].add(item['state'])
128
+ row['started'] = item['started'] if row['started'] is None else min(row['started'], item['started'])
129
+ out = []
130
+ for row in grouped.values():
131
+ states = sorted(row.pop('states'))
132
+ row['states'] = states
133
+ bad = [state for state in states if state in BAD_STATES]
134
+ script_run = row.pop('script_run')
135
+ row['kind'] = 'platform' if row['platform'] else 'routine' if script_run else 'external'
136
+ row.update(state=(bad or states or ['unknown'])[0], level='error' if bad else 'running',
137
+ cpu=round(row['cpu'], 1), memory_mb=int(row['memory_mb']), pids=sorted(row['pids'])[:PIDS_SHOWN],
138
+ started=_iso(row['started']), commandable=row['kind'] == 'routine')
139
+ out.append(row)
140
+ return sorted(out, key=lambda row: (row['script'].lower(), row['key']))
141
+
142
+
143
+ def view():
144
+ """Everything the page shows."""
145
+ from SharedData.Web import workerpool
146
+ found = rows()
147
+ shown = [{key: value for key, value in row.items() if not key.startswith('_')} for row in found]
148
+ totals = dict(routines=len(shown), processes=sum(row['processes'] for row in shown), cpu=round(sum(row['cpu'] for row in shown), 1),
149
+ memory_mb=sum(row['memory_mb'] for row in shown), failing=sum(row['level'] == 'error' for row in shown))
150
+ return dict(worker=workerpool.local_worker().upper(), read_at=_iso(time.time()), viewer=sched.viewer(), rows=shown, totals=totals)
151
+
152
+
153
+ def command(key, job):
154
+ """The worker command for *job* on the routine whose card has this key. LookupError when it
155
+ no longer runs here, PermissionError for a platform process, ValueError for a wrong job."""
156
+ from SharedData.Web import workerpool
157
+ if not isinstance(job, str) or job not in JOBS:
158
+ raise ValueError('job must be one of: ' + ', '.join(JOBS))
159
+ found = [row for row in rows() if row['key'] == key] if isinstance(key, str) else []
160
+ if not found:
161
+ raise LookupError('that routine no longer runs on this server')
162
+ row = found[0]
163
+ if row['kind'] != 'routine' or is_scheduler(row['script']):
164
+ raise PermissionError('this process is not a routine of the Worker (a platform process, or an installed program): it is not restarted or stopped from here')
165
+ command = {'sender': 'MASTER', 'job': JOBS[job], 'target': workerpool.local_worker(), 'repo': row['repo'], 'routine': row['routine']}
166
+ if row['branch'] and row['branch'] != 'nan':
167
+ command['branch'] = row['branch']
168
+ if row['_args'] and str(row['_args']) != 'nan':
169
+ command['args'] = str(row['_args'])
170
+ return command
@@ -31,7 +31,7 @@ def csrf_valid():
31
31
  return bool(provided and expected and hmac.compare_digest(provided.encode(), expected.encode()))
32
32
 
33
33
 
34
- ADMIN_PAGES = {'users': 'Users', 'data': 'Data', 'routes': 'Routes', 'workers': 'Workers', 'sched': 'Schedules', 'logs': 'Logs',
34
+ ADMIN_PAGES = {'users': 'Users', 'data': 'Data', 'routes': 'Routes', 'workers': 'Workers', 'sched': 'Schedules', 'proc': 'Processes', 'logs': 'Logs',
35
35
  'workerpool': 'Worker pool', 'redis': 'Redis', 'kafka': 'Kafka', 'mongo': 'MongoDB'}
36
36
 
37
37
 
@@ -575,6 +575,36 @@ def init_standalone_routes(app, shdata):
575
575
  Logger.log.info('SCHED: %s of %s sent by %s' % (data.get('job'), data.get('routine'), g.get('current_identity')))
576
576
  return jsonify(sent={key: value for key, value in command.items() if key != 'args'}) # args may hold a secret
577
577
 
578
+ # PROC: the routines whose processes run on this server, and restart/stop of one.
579
+ @bp.get('/api/web/proc')
580
+ def proc_state():
581
+ from SharedData.Web import proc
582
+ denied = route_admin()
583
+ if denied:
584
+ return denied
585
+ return jsonify(proc.view())
586
+
587
+ @bp.post('/api/web/proc/command')
588
+ def proc_command():
589
+ from SharedData.Logger import Logger
590
+ from SharedData.Routines.WorkerPool import WorkerPool
591
+ from SharedData.Web import proc
592
+ denied = route_admin()
593
+ if denied:
594
+ return denied
595
+ data = request.get_json(silent=True) or {}
596
+ try:
597
+ command = proc.command(data.get('key'), data.get('job'))
598
+ except LookupError as exc:
599
+ return jsonify(error=str(exc)), 404
600
+ except PermissionError as exc:
601
+ return jsonify(error=str(exc)), 409
602
+ except ValueError as exc:
603
+ return jsonify(error=str(exc)), 400
604
+ WorkerPool.post_commands(shdata, [dict(command)])
605
+ Logger.log.info('PROC: %s of %s sent by %s' % (data.get('job'), command['repo'] + '/' + command['routine'], g.get('current_identity')))
606
+ return jsonify(sent={key: value for key, value in command.items() if key != 'args'}) # args may hold a secret
607
+
578
608
  def _jobs_list(args):
579
609
  """What the Jobs tab's addresses have in common: ?month=YYYYMM ?status=A,B ?q=text."""
580
610
  return {'month': args.get('month') or None, 'statuses': [s for s in args.get('status', '').split(',') if s], 'text': args.get('q') or None}
@@ -385,6 +385,19 @@ body.resizing { cursor: col-resize; user-select: none; }
385
385
  .sched-fact .hint.inline { flex-basis: 100%; }
386
386
  }
387
387
 
388
+ /* PROC: a card for each routine running here, in SCHED's colours */
389
+ .sched-main.proc-main { grid-template-columns: .6rem 5.4rem minmax(10rem, 1fr) 4.4rem 5.2rem 2.6rem 3.6rem; }
390
+ .proc-num { color: var(--ink); font-size: .82rem; text-align: right; font-variant-numeric: tabular-nums; white-space: nowrap; }
391
+ .proc-cpu[data-hot] { color: #ffd479; font-weight: 600; }
392
+ @media (min-width: 721px) { .proc-row { grid-template-columns: minmax(0, 1fr) 8.2rem; } .proc-row .sched-actions { justify-content: flex-end; } }
393
+ .proc-platform { font-size: .72rem; }
394
+ .proc-procs { color: var(--muted); }
395
+ @media (max-width: 720px) {
396
+ .sched-main.proc-main { grid-template-columns: .6rem auto minmax(0, 1fr) auto; grid-template-areas: "dot status name up" "dot cpu ram procs"; gap: .1rem .45rem; }
397
+ .proc-main .sched-dot { grid-area: dot; } .proc-main .sched-status { grid-area: status; } .proc-main .sched-name { grid-area: name; } .proc-main .proc-up { grid-area: up; }
398
+ .proc-main .proc-cpu { grid-area: cpu; text-align: left; } .proc-main .proc-ram { grid-area: ram; text-align: left; } .proc-main .proc-procs { grid-area: procs; text-align: left; }
399
+ }
400
+
388
401
  /* WORKERS: how each worker is doing, from its heartbeat */
389
402
  /* WORKERS, Status: a card for each worker, as the Dashboard's workers tab shows them. */
390
403
  .worker-hosts { display: grid; gap: .35rem; }
@@ -1544,6 +1544,138 @@ if (schedList) {
1544
1544
  if (view.tab === 'jobs') loadJobs();
1545
1545
  }
1546
1546
 
1547
+ // PROC: what each routine running on this server uses, read again every 5 seconds; restart and stop of a routine.
1548
+ const procList = document.querySelector('#proc-list');
1549
+ if (procList) {
1550
+ const KEY = 'shareddata.proc', SORTS = ['name', 'cpu', 'memory', 'started'];
1551
+ const saved = (() => { try { return JSON.parse(localStorage.getItem(KEY)) || {}; } catch (_) { return {}; } })();
1552
+ const view = {q: '', failing: saved.failing === true, sort: SORTS.includes(saved.sort) ? saved.sort : 'name', open: new Set(), filters: saved.filters === true};
1553
+ let data = null, timer;
1554
+ const sent = {};
1555
+ const summary = document.querySelector('#proc-summary'), tools = document.querySelector('#proc-tools'), filter = document.querySelector('#proc-q');
1556
+ const pills = document.querySelector('#proc-levels'), sorting = document.querySelector('#proc-sort'), empty = document.querySelector('#proc-empty');
1557
+ const failed = document.querySelector('#proc-error'), filtersBtn = document.querySelector('#proc-filters');
1558
+ const remember = () => localStorage.setItem(KEY, JSON.stringify({failing: view.failing, sort: view.sort, filters: view.filters}));
1559
+ const clock = iso => iso ? new Date(iso).toLocaleTimeString([], {hour12: false}) : '';
1560
+ const memory = mb => mb >= 1024 ? (mb / 1024).toFixed(1) + ' GB' : mb + ' MB';
1561
+ const up = row => spanText((Date.now() - Date.parse(row.started)) / 1000);
1562
+ function paintFilters() {
1563
+ tools.hidden = !view.filters;
1564
+ filtersButton(filtersBtn, view.filters, (view.q ? 1 : 0) + (view.failing ? 1 : 0) + (view.sort !== 'name' ? 1 : 0));
1565
+ }
1566
+ function openFilters(open) { view.filters = open; remember(); paintFilters(); }
1567
+ filtersBtn.addEventListener('click', () => { openFilters(!view.filters); if (view.filters) filter.focus(); });
1568
+ function matches(row) {
1569
+ if (view.failing && row.level !== 'error') return false;
1570
+ return !view.q || [row.script, row.repo, row.branch, row.args, row.routine].some(text => String(text).toLowerCase().includes(view.q));
1571
+ }
1572
+ const ORDER = {
1573
+ name: (a, b) => a.script.localeCompare(b.script, undefined, {sensitivity: 'base'}),
1574
+ cpu: (a, b) => b.cpu - a.cpu || ORDER.name(a, b),
1575
+ memory: (a, b) => b.memory_mb - a.memory_mb || ORDER.name(a, b),
1576
+ started: (a, b) => Date.parse(b.started) - Date.parse(a.started) || ORDER.name(a, b)};
1577
+ function name(row) { // 'Repo#branch/folder/' muted, the code's own name strong
1578
+ const cut = row.script.includes('/') ? row.script.lastIndexOf('/') + 1 : 0;
1579
+ return el('span', {class: 'sched-name'}, cut ? el('span', {class: 'sched-path'}, row.script.slice(0, cut)) : null, el('strong', {}, row.script.slice(cut)));
1580
+ }
1581
+ function ask(row, job) {
1582
+ const verb = job === 'restart' ? 'Restart' : 'Stop';
1583
+ const box = el('dialog', {class: 'log-popup sched-confirm'},
1584
+ el('p', {}, el('strong', {}, `${verb} this routine?`)),
1585
+ el('p', {class: 'sched-routine'}, row.script + (row.args ? ' ' + row.args : '')),
1586
+ el('p', {class: 'hint'}, `on ${data.worker}` + (row.processes > 1 ? ` · ${row.processes} processes` : '')),
1587
+ el('p', {class: 'hint'}, job === 'restart' ? 'The worker ends the routine and starts it again with the newest code.'
1588
+ : 'The worker ends the routine. The Scheduler starts a realtime routine again once it has been silent for a while (45 seconds to 5 minutes): to keep it stopped, take it out of the schedule.'),
1589
+ data.viewer ? el('p', {class: 'hint', 'data-viewer': ''}, 'This server is a viewer: the command is recorded here and reaches no worker, so the routine goes on as it is.') : null,
1590
+ el('div', {class: 'actions'},
1591
+ el('button', {type: 'button', autofocus: true, onclick: () => box.close()}, 'Cancel'),
1592
+ el('button', {type: 'button', class: 'danger', 'data-confirm': job, onclick: async event => {
1593
+ event.target.disabled = true;
1594
+ try {
1595
+ await send('POST', '/api/web/proc/command', {key: row.key, job});
1596
+ sent[row.key] = {job, at: Date.now(), started: row.started};
1597
+ result.textContent = `${verb} sent: ${row.script}` + (data.viewer ? ' (on this viewer only: no worker receives it)' : '');
1598
+ } catch (error) { failure(error); }
1599
+ box.close(); paint();
1600
+ }}, verb)));
1601
+ box.addEventListener('click', event => { if (event.target === box) box.close(); });
1602
+ box.addEventListener('close', () => box.remove());
1603
+ document.body.append(box); box.showModal();
1604
+ }
1605
+ function details(row) {
1606
+ const fact = (label, value) => value || value === 0 ? el('div', {class: 'sched-fact'}, el('span', {class: 'hint inline'}, label), el('span', {}, String(value))) : null;
1607
+ const more = row.processes - row.pids.length;
1608
+ return el('div', {class: 'sched-details'},
1609
+ fact('Repository', row.repo + (row.branch ? ' · branch ' + row.branch : '')), fact('Code', row.routine), fact('Arguments', row.args),
1610
+ fact(row.processes > 1 ? 'Processes' : 'Process', row.pids.join(', ') + (more > 0 ? ` and ${more} more` : '')),
1611
+ fact('Threads', row.threads), fact('Open files', row.files), fact('State', row.states.join(', ')), fact('Started', clock(row.started)),
1612
+ row.commandable ? null : el('p', {class: 'hint'}, row.kind === 'platform' ? 'A platform process: its Worker keeps it running, so it is not restarted or stopped from here.'
1613
+ : 'An installed program, not a script the Worker runs: it is listed for what it uses and is not restarted or stopped from here.'),
1614
+ el('div', {class: 'actions'}, el('button', {type: 'button', onclick: async event => {
1615
+ try { await navigator.clipboard.writeText(row.script); event.target.textContent = 'Copied'; } catch (_) { event.target.textContent = row.script; }
1616
+ }}, 'Copy routine name')));
1617
+ }
1618
+ function line(row) {
1619
+ const open = view.open.has(row.key), mark = sent[row.key];
1620
+ const main = el('div', {class: 'sched-main proc-main', role: 'button', tabindex: '0', 'aria-expanded': String(open),
1621
+ onclick: () => { open ? view.open.delete(row.key) : view.open.add(row.key); paint(); },
1622
+ onkeydown: event => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); event.currentTarget.click(); } }},
1623
+ el('span', {class: 'sched-dot', 'aria-hidden': 'true'}),
1624
+ el('span', {class: 'sched-status'}, row.level === 'error' ? row.state.toUpperCase() : 'RUNNING'),
1625
+ name(row),
1626
+ el('span', {class: 'proc-num proc-cpu', title: 'CPU: percent of one core', 'data-hot': row.cpu >= 80 ? '' : null}, row.cpu.toFixed(1) + '%'),
1627
+ el('span', {class: 'proc-num proc-ram', title: 'Memory in use'}, memory(row.memory_mb)),
1628
+ el('span', {class: 'proc-num proc-procs', title: 'Processes'}, row.processes > 1 ? '×' + row.processes : ''),
1629
+ el('span', {class: 'sched-age proc-up', 'data-started': row.started, title: 'Running for'}, up(row)));
1630
+ return el('div', {class: 'sched-row proc-row', 'data-level': row.level, 'data-key': row.key}, main,
1631
+ el('span', {class: 'sched-actions'}, mark ? el('span', {class: 'sched-sent'}, `${mark.job} sent ${clock(new Date(mark.at).toISOString())}`) : null,
1632
+ ...(row.commandable ? [el('button', {type: 'button', 'data-job': 'restart', onclick: () => ask(row, 'restart')}, 'Restart'),
1633
+ el('button', {type: 'button', 'data-job': 'stop', onclick: () => ask(row, 'stop')}, 'Stop')] : [el('span', {class: 'hint proc-platform'}, row.kind)])),
1634
+ open ? details(row) : null);
1635
+ }
1636
+ function paint() {
1637
+ if (!data) return;
1638
+ const rows = data.rows, totals = data.totals, bad = rows.filter(row => row.level === 'error').length;
1639
+ summary.textContent = `${data.worker} · ${totals.routines} ${totals.routines === 1 ? 'routine' : 'routines'} · ${totals.processes} ${totals.processes === 1 ? 'process' : 'processes'} · CPU ${totals.cpu.toFixed(0)}% · ${memory(totals.memory_mb)}` + (bad ? ` · ${bad} failing` : '');
1640
+ pills.replaceChildren(...(bad || view.failing ? [el('button', {type: 'button', class: 'pill sched-level', 'data-level': 'error', 'aria-pressed': String(view.failing),
1641
+ onclick: () => { view.failing = !view.failing; remember(); paint(); }}, `Failing ${bad}`)] : []));
1642
+ paintFilters();
1643
+ sorting.value = view.sort;
1644
+ const shown = rows.filter(matches).sort(ORDER[view.sort]);
1645
+ procList.replaceChildren(...shown.map(line));
1646
+ empty.hidden = shown.length > 0;
1647
+ empty.textContent = !rows.length ? 'No routine is running on this server.' : 'No routine matches the filter.';
1648
+ }
1649
+ function times() { // once a second: uptimes go on counting between two readings
1650
+ for (const node of procList.querySelectorAll('.proc-up[data-started]')) node.textContent = spanText((Date.now() - Date.parse(node.dataset.started)) / 1000);
1651
+ }
1652
+ async function load() {
1653
+ clearTimeout(timer);
1654
+ try {
1655
+ const next = await call('/api/web/proc');
1656
+ data = next; failed.hidden = true;
1657
+ for (const [key, mark] of Object.entries(sent)) { // a mark stays until the routine has started again, or a minute
1658
+ const row = data.rows.find(candidate => candidate.key === key);
1659
+ if (!row || row.started !== mark.started || Date.now() - mark.at > 60000) delete sent[key];
1660
+ }
1661
+ if (!String(getSelection()) || !procList.contains(getSelection().anchorNode)) paint();
1662
+ } catch (error) { failed.hidden = false; failed.textContent = 'Cannot read the processes: ' + error.message; }
1663
+ if (!document.hidden) timer = setTimeout(load, 5000);
1664
+ }
1665
+ filter.addEventListener('input', () => { view.q = filter.value.trim().toLowerCase(); paint(); });
1666
+ sorting.addEventListener('change', () => { view.sort = sorting.value; remember(); paint(); });
1667
+ document.addEventListener('keydown', event => {
1668
+ if (event.ctrlKey || event.metaKey || event.altKey || document.querySelector('dialog[open]')) return;
1669
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(event.target.tagName)) { if (event.key === 'Escape') event.target.blur(); return; }
1670
+ if (event.key === '/') { event.preventDefault(); openFilters(true); filter.focus(); }
1671
+ else if (event.key.toLowerCase() === 'f') pills.querySelector('[data-level=error]')?.click();
1672
+ });
1673
+ document.addEventListener('visibilitychange', () => { if (!document.hidden) load(); });
1674
+ setInterval(times, 1000);
1675
+ paintFilters();
1676
+ load();
1677
+ }
1678
+
1547
1679
  // Shared by WORKERPOOL and REDIS: a node's children without the ones that are not there, and
1548
1680
  // sizes and spans as a person reads them.
1549
1681
  function fill(node, ...children) { node.replaceChildren(...children.filter(child => child != null)); }
@@ -94,6 +94,13 @@
94
94
  <p><button id="jobs-more" type="button" hidden>Show more</button></p>
95
95
  </div>
96
96
  </section>{% endif %}
97
+ {% if section == 'proc' %}<section id="page-proc" class="page tight sched proc">
98
+ <div class="head"><h2>Processes</h2><button id="proc-filters" class="filters-toggle" type="button" aria-expanded="false">Filters</button><span id="proc-summary" class="hint" role="status"></span></div>
99
+ <div id="proc-tools" class="toolbar sched-toolbar" hidden><input id="proc-q" type="search" placeholder="Filter: routine, repository, branch or arguments" aria-label="Filter routines"><span id="proc-levels" class="pills" role="group" aria-label="States shown"></span><select id="proc-sort" aria-label="Sort by"><option value="name">By name</option><option value="cpu">By CPU</option><option value="memory">By memory</option><option value="started">Newest first</option></select></div>
100
+ <p id="proc-error" class="sched-stale" role="alert" hidden></p>
101
+ <div id="proc-list" class="sched-list" aria-live="off"></div>
102
+ <p id="proc-empty" class="hint" hidden></p>
103
+ </section>{% endif %}
97
104
  {% if section == 'workerpool' %}<section id="page-workerpool" class="page tight sched workerpool">
98
105
  <div class="head"><h2>Worker pool</h2><span id="wp-count" class="hint" role="status"></span><button id="wp-repos-btn" type="button" aria-expanded="false" aria-controls="wp-repos">Repositories</button><button id="wp-procs-btn" type="button" aria-expanded="false" aria-controls="wp-procs">Processes</button></div>
99
106
  <div id="wp-workers" class="wp-workers" role="listbox" aria-label="Where the commands go"></div>
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shareddata
3
- Version: 6.85.0
3
+ Version: 6.85.1
4
4
  Summary: Memory Mapped / Shared Memory Database with S3 repository
5
5
  Home-page: https://shareddata.net
6
6
  Author: shareddata
@@ -132,6 +132,7 @@ src/SharedData/Web/containers.py
132
132
  src/SharedData/Web/identity.py
133
133
  src/SharedData/Web/kafka_view.py
134
134
  src/SharedData/Web/mongo_view.py
135
+ src/SharedData/Web/proc.py
135
136
  src/SharedData/Web/redis_view.py
136
137
  src/SharedData/Web/route_health.py
137
138
  src/SharedData/Web/route_registry.py
File without changes
File without changes
File without changes
File without changes
File without changes