shareddata 6.85.0__tar.gz → 6.85.2__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.2}/PKG-INFO +1 -1
  2. {shareddata-6.85.0 → shareddata-6.85.2}/setup.py +1 -1
  3. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/apidocs.yml +1 -1
  4. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/resolve.py +11 -3
  5. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Redaction.py +29 -0
  6. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Table.py +70 -3
  7. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableDisk.py +55 -18
  8. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/config.py +1 -0
  9. shareddata-6.85.2/src/SharedData/Web/proc.py +170 -0
  10. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/standalone.py +31 -1
  11. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/standalone.css +13 -0
  12. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/standalone.js +132 -0
  13. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/standalone.html +7 -0
  14. {shareddata-6.85.0 → shareddata-6.85.2/src/shareddata.egg-info}/PKG-INFO +1 -1
  15. {shareddata-6.85.0 → shareddata-6.85.2}/src/shareddata.egg-info/SOURCES.txt +1 -0
  16. {shareddata-6.85.0 → shareddata-6.85.2}/LICENSE +0 -0
  17. {shareddata-6.85.0 → shareddata-6.85.2}/MANIFEST.in +0 -0
  18. {shareddata-6.85.0 → shareddata-6.85.2}/README.md +0 -0
  19. {shareddata-6.85.0 → shareddata-6.85.2}/pyproject.toml +0 -0
  20. {shareddata-6.85.0 → shareddata-6.85.2}/setup.cfg +0 -0
  21. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/ServerGunicorn.py +0 -0
  22. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/ServerWaitress.py +0 -0
  23. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/__init__.py +0 -0
  24. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/apidocs-dark.css +0 -0
  25. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/auth.py +0 -0
  26. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/constants.py +0 -0
  27. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/download_paths.py +0 -0
  28. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/download_slots.py +0 -0
  29. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/install/worker.ps1 +0 -0
  30. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/install/worker.sh +0 -0
  31. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/listcache.py +0 -0
  32. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/__init__.py +0 -0
  33. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/cache.py +0 -0
  34. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/collections.py +0 -0
  35. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/data.py +0 -0
  36. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/downloads.py +0 -0
  37. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/metadata.py +0 -0
  38. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/operations.py +0 -0
  39. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/schedules.py +0 -0
  40. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/sqlite.py +0 -0
  41. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/system.py +0 -0
  42. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/tables.py +0 -0
  43. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/timeseries.py +0 -0
  44. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/web_routes.py +0 -0
  45. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/routes/workers.py +0 -0
  46. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/API/utils.py +0 -0
  47. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Auth/__init__.py +0 -0
  48. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Auth/bootstrap.py +0 -0
  49. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Auth/migrate.py +0 -0
  50. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Auth/store.py +0 -0
  51. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Auth/workers.py +0 -0
  52. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/CacheRedis.py +0 -0
  53. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/CollectionMongoDB.py +0 -0
  54. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/__init__.py +0 -0
  55. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/catalog.py +0 -0
  56. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/config.py +0 -0
  57. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/errors.py +0 -0
  58. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/indexes.py +0 -0
  59. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/readers.py +0 -0
  60. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/DataAPI/service.py +0 -0
  61. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Database.py +0 -0
  62. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Defaults.py +0 -0
  63. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/AWSEC2.py +0 -0
  64. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/AWSS3.py +0 -0
  65. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/AutoDocstrings.py +0 -0
  66. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/ClientAPI.py +0 -0
  67. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/ClientSocket.py +0 -0
  68. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/ClientWebSocket.py +0 -0
  69. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/LogHandlerAPI.py +0 -0
  70. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/LogReader.py +0 -0
  71. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/MongoDBClient.py +0 -0
  72. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/SaveTables.py +0 -0
  73. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/ServerSocket.py +0 -0
  74. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/ServerWebSocket.py +0 -0
  75. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/StreamsCache.py +0 -0
  76. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/StreamsPersist.py +0 -0
  77. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/SyncTable.py +0 -0
  78. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/TunnelWebSocket.py +0 -0
  79. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/IO/__init__.py +0 -0
  80. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Listing.py +0 -0
  81. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/LockWaiters.py +0 -0
  82. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Logger.py +0 -0
  83. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/MCP/__init__.py +0 -0
  84. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/MCP/__main__.py +0 -0
  85. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/MCP/client.py +0 -0
  86. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/MCP/server.py +0 -0
  87. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Metadata.py +0 -0
  88. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/MultiProc.py +0 -0
  89. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/OpenFIGI.py +0 -0
  90. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/ArchiveOldCollections.py +0 -0
  91. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/BatchJob.py +0 -0
  92. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/ContainerHealth.py +0 -0
  93. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/JobStore.py +0 -0
  94. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/RoutineCommand.py +0 -0
  95. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/RoutineOrigin.py +0 -0
  96. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/Schedule.py +0 -0
  97. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/ScheduleMirror.py +0 -0
  98. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/ScheduleMonitor.py +0 -0
  99. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/ScheduleStore.py +0 -0
  100. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/Scheduler.py +0 -0
  101. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/UpdateActiveJobs.py +0 -0
  102. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/Worker.py +0 -0
  103. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/WorkerBootstrap.py +0 -0
  104. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/WorkerLib.py +0 -0
  105. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/WorkerPool.py +0 -0
  106. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Routines/__init__.py +0 -0
  107. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/SQLite.py +0 -0
  108. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/SharedData.py +0 -0
  109. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/SharedNumpy.py +0 -0
  110. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/StreamKafka.py +0 -0
  111. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Symbol.py +0 -0
  112. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndex.py +0 -0
  113. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndexJitFunctions.py +0 -0
  114. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndexJitFunctionsManual.py +0 -0
  115. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndexJitGenerate.py +0 -0
  116. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndexJitHash.py +0 -0
  117. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableIndexJitLoc.py +0 -0
  118. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableProtocol.py +0 -0
  119. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableRead.py +0 -0
  120. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TableRewrite.py +0 -0
  121. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TimeSeriesDisk.py +0 -0
  122. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/TimeseriesContainer.py +0 -0
  123. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Users.py +0 -0
  124. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Utils.py +0 -0
  125. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/ADDING_A_PAGE.md +0 -0
  126. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/__init__.py +0 -0
  127. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/activity_logger.py +0 -0
  128. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/app.py +0 -0
  129. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/dashAgGridComponentFunctions.js +0 -0
  130. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/dashAgGridFunctions.js +0 -0
  131. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/google_logo.png +0 -0
  132. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/kafka_keys.js +0 -0
  133. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/logs.css +0 -0
  134. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/sched.css +0 -0
  135. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/assets/styles.css +0 -0
  136. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/auth.py +0 -0
  137. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/backends.py +0 -0
  138. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/containers.py +0 -0
  139. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/identity.py +0 -0
  140. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/kafka_view.py +0 -0
  141. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/mongo_view.py +0 -0
  142. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/redis_view.py +0 -0
  143. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/route_health.py +0 -0
  144. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/route_registry.py +0 -0
  145. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/sched.py +0 -0
  146. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/schedule_editor.py +0 -0
  147. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/server.py +0 -0
  148. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/services.py +0 -0
  149. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/shell.py +0 -0
  150. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/account.js +0 -0
  151. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/custom.css +0 -0
  152. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/favicon.ico +0 -0
  153. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/favicon.png +0 -0
  154. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/logo.png +0 -0
  155. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/logo_96x96.png +0 -0
  156. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/shareddata-mark.svg +0 -0
  157. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/shell.css +0 -0
  158. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/static/shell.js +0 -0
  159. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/suggest.py +0 -0
  160. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/account.html +0 -0
  161. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/base.html +0 -0
  162. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/claim.html +0 -0
  163. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/claim_done.html +0 -0
  164. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/dash.html +0 -0
  165. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/index.html +0 -0
  166. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/login.html +0 -0
  167. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/login_2fa.html +0 -0
  168. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/partials/navbar.html +0 -0
  169. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/partials/scripts.html +0 -0
  170. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/recovery_codes.html +0 -0
  171. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/templates/shell.html +0 -0
  172. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/twofactor.py +0 -0
  173. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/utils.py +0 -0
  174. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/Web/workerpool.py +0 -0
  175. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/__init__.py +0 -0
  176. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/build_info.py +0 -0
  177. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/fleet.py +0 -0
  178. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/preflight.py +0 -0
  179. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/private_files.py +0 -0
  180. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/release_backup.py +0 -0
  181. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/server.py +0 -0
  182. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/services.py +0 -0
  183. {shareddata-6.85.0 → shareddata-6.85.2}/src/SharedData/sharedmutexwin.pyd +0 -0
  184. {shareddata-6.85.0 → shareddata-6.85.2}/src/shareddata.egg-info/dependency_links.txt +0 -0
  185. {shareddata-6.85.0 → shareddata-6.85.2}/src/shareddata.egg-info/entry_points.txt +0 -0
  186. {shareddata-6.85.0 → shareddata-6.85.2}/src/shareddata.egg-info/requires.txt +0 -0
  187. {shareddata-6.85.0 → shareddata-6.85.2}/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.2
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.2',
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
  """
@@ -813,6 +865,21 @@ class Table:
813
865
  raise RewriteError('automatic disk header migration requires staged publication')
814
866
 
815
867
  ############### DOWNLOAD ###############
868
+ def _remote_is_newer(self):
869
+ """True when the remote head or tail is newer than this table's (what S3Download would
870
+ fetch). Nothing newer, no remote copy, or S3 switched off: False, and the local image stays."""
871
+ from SharedData.services import s3_enabled
872
+ if not s3_enabled() or self.hdr is None:
873
+ return False
874
+ head = S3GetMtime(str(self.headpath) + '.gzip', self.database_folder)
875
+ if head is not None and head > float(self.hdr['mtimehead']):
876
+ return True
877
+ if int(self.hdr['hastail']) == 1:
878
+ tail = S3GetMtime(str(self.tailpath) + '.gzip', self.database_folder)
879
+ if tail is not None and tail > float(self.hdr['mtimetail']):
880
+ return True
881
+ return False
882
+
816
883
  @structural
817
884
  def download(self):
818
885
  """
@@ -263,27 +263,64 @@ class TableDisk(Table):
263
263
  @structural
264
264
  def reload(self):
265
265
  """
266
- Reloads the table data from disk by performing a sequence of operations to ensure data consistency and resource management.
267
-
268
- The method:
269
- - Acquires necessary locks or resources to safely perform the reload.
270
- - Frees current resources without acquiring new ones to prepare for fresh data.
271
- - Downloads the latest table data from disk.
272
- - Allocates memory for the newly downloaded data.
273
- - If an index exists, resets its creation flag and initializes the index.
274
- - Updates shareddata references to point to the reloaded table.
275
- - Marks the table as loaded in both header and mutex states.
276
- - Handles and logs any exceptions that occur during the reload process.
277
- - Ensures that all acquired resources are released after the operation, regardless of success or failure.
278
-
266
+ Replace the local image with the remote copy when that is newer.
267
+
268
+ Nothing newer remotely (or no remote copy, or S3 off) leaves the local table as it is, so
269
+ rows not uploaded yet are never discarded. Otherwise the whole remote image is built beside
270
+ data.bin under a stage name and installed by one os.replace, inside the caller's STRUCT
271
+ section: a failure before the replace leaves the old image untouched, and the epoch
272
+ published on leaving the section makes other processes reopen the new file. The old index
273
+ files describe the old rows, so they are removed and rebuilt.
274
+
279
275
  Returns:
280
276
  The records of the reloaded table.
281
277
  """
282
- from SharedData.TableRewrite import RewriteError
283
- raise RewriteError('disk reload requires staged remote replacement')
278
+ import stat
279
+ import uuid
280
+ from SharedData.TableRewrite import fsync_directory, read_layout, require_rewrite_support, sync_file
281
+ if not self._remote_is_newer():
282
+ return self.records
283
+ require_rewrite_support()
284
+ final = self.filepath
285
+ old = os.stat(final)
286
+ for leftover in final.parent.glob('data.bin.reload.*'): # the writer lock is held: a stage left by a dead reload
287
+ leftover.unlink()
288
+ stage = final.parent / ('data.bin.reload.' + uuid.uuid4().hex)
289
+ saved = (self.hdr, self.size, getattr(self, 'orig_hdr', None))
290
+ self.filepath, self.hdr = stage, None # None: download the remote image, not what is newer than the local one
291
+ self._bootstrap_record_download = True
284
292
  try:
285
- self.free(acquire=False)
286
- self.download()
293
+ self._download_empty_image()
294
+ if self.hdr is None or not stage.is_file():
295
+ raise Exception('the remote copy of %s disappeared during the reload' % self.relpath)
296
+ sync_file(stage)
297
+ with open(stage, 'rb') as stream:
298
+ downloaded, _ = read_layout(stream)
299
+ os.chmod(stage, stat.S_IMODE(old.st_mode))
300
+ try:
301
+ os.chown(stage, old.st_uid, old.st_gid)
302
+ except OSError:
303
+ pass
304
+ os.replace(stage, final)
305
+ except BaseException:
306
+ self.filepath = final
307
+ self.hdr, self.size, self.orig_hdr = saved
308
+ try:
309
+ stage.unlink()
310
+ except FileNotFoundError:
311
+ pass
312
+ raise
313
+ finally:
314
+ self._bootstrap_record_download = False
315
+ self.filepath = final
316
+ fsync_directory(final.parent)
317
+ try:
318
+ self.free(acquire=False) # the old mappings and index arrays, flushed first
319
+ for name in ('dateidx.bin', 'pkey.bin', 'symbolidx.bin', 'portidx.bin'):
320
+ try:
321
+ (final.parent / name).unlink()
322
+ except FileNotFoundError:
323
+ pass
287
324
  self.malloc()
288
325
  if self.hasindex:
289
326
  self.hdr['isidxcreated'] = 0
@@ -296,7 +333,7 @@ class TableDisk(Table):
296
333
  Logger.log.error(errmsg)
297
334
  raise Exception(errmsg)
298
335
  return self.records
299
-
336
+
300
337
  ############### DELETE ###############
301
338
  def delete_local(self):
302
339
  """
@@ -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; }