stoplight 5.8.3 → 6.0.0.rc2

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 (323) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -1
  3. data/LICENSE.md +1 -0
  4. data/README.md +132 -79
  5. data/UPGRADING.md +265 -6
  6. data/lib/stoplight/admin/actions/action.rb +4 -12
  7. data/lib/stoplight/admin/actions/lock.rb +12 -9
  8. data/lib/stoplight/admin/actions/lock_all.rb +27 -0
  9. data/lib/stoplight/admin/actions/remove.rb +12 -9
  10. data/lib/stoplight/admin/actions/stats.rb +15 -12
  11. data/lib/stoplight/admin/actions/unlock.rb +10 -9
  12. data/lib/stoplight/admin/assets/apple-touch-icon.png +0 -0
  13. data/lib/stoplight/admin/assets/favicon.ico +0 -0
  14. data/lib/stoplight/admin/assets/flowbite.min.css +2 -0
  15. data/lib/stoplight/admin/assets/flowbite.min.js +1 -0
  16. data/lib/stoplight/admin/assets/icon.svg +7 -0
  17. data/lib/stoplight/admin/assets/turbo.es2017-esm.min.js +34 -0
  18. data/lib/stoplight/admin/config_registry.rb +40 -0
  19. data/lib/stoplight/admin/dependencies.rb +29 -28
  20. data/lib/stoplight/admin/helpers.rb +50 -17
  21. data/lib/stoplight/admin/light_view.rb +184 -0
  22. data/lib/stoplight/admin/lights_stats.rb +7 -9
  23. data/lib/stoplight/admin/views/_card.erb +19 -21
  24. data/lib/stoplight/admin/views/index.erb +9 -3
  25. data/lib/stoplight/admin/views/layout.erb +45 -13
  26. data/lib/stoplight/admin.rb +110 -21
  27. data/lib/stoplight/data_store.rb +18 -8
  28. data/lib/stoplight/domain/config.rb +4 -1
  29. data/lib/stoplight/domain/error_tracking_policy.rb +9 -0
  30. data/lib/stoplight/domain/failure.rb +4 -0
  31. data/lib/stoplight/domain/id.rb +13 -0
  32. data/lib/stoplight/domain/light.rb +35 -77
  33. data/lib/stoplight/domain/lock_control.rb +55 -0
  34. data/lib/stoplight/domain/matcher_validator.rb +24 -0
  35. data/lib/stoplight/domain/state_snapshot.rb +2 -6
  36. data/lib/stoplight/domain/strategies/green_run_strategy.rb +33 -19
  37. data/lib/stoplight/domain/strategies/red_run_strategy.rb +8 -2
  38. data/lib/stoplight/domain/strategies/yellow_run_strategy.rb +56 -32
  39. data/lib/stoplight/domain/telemetry/bus.rb +90 -0
  40. data/lib/stoplight/domain/telemetry/consumer.rb +18 -0
  41. data/lib/stoplight/domain/telemetry/emitter.rb +45 -0
  42. data/lib/stoplight/domain/telemetry/envelope.rb +15 -0
  43. data/lib/stoplight/domain/telemetry/failure.rb +13 -0
  44. data/lib/stoplight/domain/telemetry/light_registered.rb +12 -0
  45. data/lib/stoplight/domain/telemetry/lock_changed.rb +19 -0
  46. data/lib/stoplight/domain/telemetry/metrics.rb +15 -0
  47. data/lib/stoplight/domain/telemetry/recovery_failed.rb +20 -0
  48. data/lib/stoplight/domain/telemetry/recovery_probe_completed.rb +15 -0
  49. data/lib/stoplight/domain/telemetry/recovery_started.rb +18 -0
  50. data/lib/stoplight/domain/telemetry/recovery_succeeded.rb +19 -0
  51. data/lib/stoplight/domain/telemetry/run_completed.rb +17 -0
  52. data/lib/stoplight/domain/telemetry/run_recorder.rb +58 -0
  53. data/lib/stoplight/domain/telemetry/settings.rb +21 -0
  54. data/lib/stoplight/domain/telemetry/state_transitioned.rb +11 -0
  55. data/lib/stoplight/domain/telemetry/subscription.rb +11 -0
  56. data/lib/stoplight/domain/telemetry/traffic_breached.rb +20 -0
  57. data/lib/stoplight/domain/tracker/recovery_probe.rb +61 -17
  58. data/lib/stoplight/domain/tracker/request.rb +17 -8
  59. data/lib/stoplight/domain/traffic_control/consecutive_errors.rb +13 -3
  60. data/lib/stoplight/domain/traffic_control/error_rate.rb +16 -12
  61. data/lib/stoplight/domain/traffic_recovery/consecutive_successes.rb +9 -3
  62. data/lib/stoplight/error.rb +10 -1
  63. data/lib/stoplight/infrastructure/config_serializer.rb +50 -0
  64. data/lib/stoplight/infrastructure/fail_safe/error_notifier.rb +25 -0
  65. data/lib/stoplight/infrastructure/fail_safe/storage/registry.rb +68 -0
  66. data/lib/stoplight/infrastructure/memory/storage/registry.rb +18 -0
  67. data/lib/stoplight/infrastructure/memory/storage/unbounded_metrics.rb +29 -32
  68. data/lib/stoplight/infrastructure/memory/storage/window_metrics/sliding_window.rb +74 -0
  69. data/lib/stoplight/infrastructure/memory/storage/window_metrics.rb +25 -24
  70. data/lib/stoplight/infrastructure/notifier/fail_safe.rb +3 -8
  71. data/lib/stoplight/infrastructure/notifier/generic.rb +3 -3
  72. data/lib/stoplight/infrastructure/redis/key.rb +25 -0
  73. data/lib/stoplight/infrastructure/redis/storage/now.lua +9 -0
  74. data/lib/stoplight/infrastructure/redis/storage/recovery_lock.rb +2 -4
  75. data/lib/stoplight/infrastructure/redis/storage/recovery_metrics.rb +1 -1
  76. data/lib/stoplight/infrastructure/redis/storage/registry.rb +73 -0
  77. data/lib/stoplight/infrastructure/redis/storage/scripting.rb +54 -3
  78. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_green.lua +3 -1
  79. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_red.lua +4 -2
  80. data/lib/stoplight/infrastructure/redis/storage/state/transition_to_yellow.lua +3 -1
  81. data/lib/stoplight/infrastructure/redis/storage/state.rb +8 -8
  82. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_failure.lua +20 -21
  83. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics/record_success.lua +11 -19
  84. data/lib/stoplight/infrastructure/redis/storage/unbounded_metrics.rb +20 -16
  85. data/lib/stoplight/infrastructure/redis/storage/window_metrics/_evict.lua +44 -0
  86. data/lib/stoplight/infrastructure/redis/storage/window_metrics/metrics_snapshot.lua +14 -21
  87. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_failure.lua +38 -36
  88. data/lib/stoplight/infrastructure/redis/storage/window_metrics/record_success.lua +23 -30
  89. data/lib/stoplight/infrastructure/redis/storage/window_metrics.rb +45 -135
  90. data/lib/stoplight/infrastructure/system_clock.rb +4 -0
  91. data/lib/stoplight/telemetry.rb +19 -0
  92. data/lib/stoplight/types.rb +15 -2
  93. data/lib/stoplight/version.rb +1 -1
  94. data/lib/stoplight/wiring/config_compatibility_validator.rb +25 -2
  95. data/lib/stoplight/wiring/default.rb +1 -1
  96. data/lib/stoplight/wiring/default_config.rb +2 -1
  97. data/lib/stoplight/wiring/default_configuration.rb +3 -2
  98. data/lib/stoplight/wiring/external_caller.rb +27 -0
  99. data/lib/stoplight/wiring/fail_safe_config.rb +11 -0
  100. data/lib/stoplight/wiring/global_state.rb +93 -0
  101. data/lib/stoplight/wiring/{light_factory → light_configuration_dsl}/traffic_control_dsl.rb +3 -5
  102. data/lib/stoplight/wiring/{light_factory → light_configuration_dsl}/traffic_recovery_dsl.rb +2 -2
  103. data/lib/stoplight/wiring/light_configuration_dsl.rb +97 -0
  104. data/lib/stoplight/wiring/light_factory.rb +154 -97
  105. data/lib/stoplight/wiring/notifier_bridge.rb +33 -0
  106. data/lib/stoplight/wiring/redis/backend.rb +1 -1
  107. data/lib/stoplight/wiring/system/storage.rb +69 -0
  108. data/lib/stoplight/wiring/system.rb +125 -56
  109. data/lib/stoplight/wiring/{configuration_dsl.rb → system_configuration_dsl.rb} +13 -43
  110. data/lib/stoplight.rb +50 -194
  111. data/sig/sinatra/base.rbs +6 -0
  112. data/sig/stdlib/securerandom.rbs +4 -0
  113. data/sig/stoplight/admin/actions/action.rbs +10 -0
  114. data/sig/stoplight/admin/actions/lock.rbs +17 -0
  115. data/sig/stoplight/admin/actions/lock_all.rbs +17 -0
  116. data/sig/stoplight/admin/actions/remove.rbs +22 -0
  117. data/sig/stoplight/admin/actions/stats.rbs +28 -0
  118. data/sig/stoplight/admin/actions/unlock.rbs +17 -0
  119. data/sig/stoplight/admin/config_registry.rbs +20 -0
  120. data/sig/stoplight/admin/dependencies.rbs +16 -0
  121. data/sig/stoplight/admin/helpers.rbs +17 -0
  122. data/sig/stoplight/admin/light_view.rbs +39 -0
  123. data/sig/stoplight/admin/lights_stats.rbs +30 -0
  124. data/sig/stoplight/admin.rbs +14 -0
  125. data/sig/stoplight/color.rbs +3 -3
  126. data/sig/{_private/stoplight/data_store.rbs → stoplight/data_store/base.rbs} +1 -0
  127. data/sig/stoplight/data_store/memory.rbs +9 -0
  128. data/sig/stoplight/data_store/redis.rbs +17 -0
  129. data/sig/{_private/stoplight → stoplight}/domain/config.rbs +28 -24
  130. data/sig/{_private/stoplight → stoplight}/domain/error_tracking_policy.rbs +4 -0
  131. data/sig/{_private/stoplight → stoplight}/domain/failure.rbs +2 -0
  132. data/sig/stoplight/domain/id.rbs +7 -0
  133. data/sig/stoplight/domain/light.rbs +39 -0
  134. data/sig/{_private/stoplight → stoplight}/domain/light_info.rbs +1 -5
  135. data/sig/stoplight/domain/lock_control.rbs +16 -0
  136. data/sig/stoplight/domain/matcher_validator.rbs +11 -0
  137. data/sig/{_private/stoplight → stoplight}/domain/ports/clock.rbs +6 -0
  138. data/sig/stoplight/domain/ports/data_store.rbs +9 -0
  139. data/sig/stoplight/domain/ports/error_notifier.rbs +12 -0
  140. data/sig/stoplight/domain/ports/exception_matcher.rbs +10 -0
  141. data/sig/{_private/stoplight → stoplight}/domain/ports/metrics_store.rbs +2 -2
  142. data/sig/stoplight/domain/ports/registry.rbs +13 -0
  143. data/sig/{_private/stoplight → stoplight}/domain/ports/run_strategy.rbs +2 -1
  144. data/sig/stoplight/domain/ports/state_transition_notifier.rbs +18 -0
  145. data/sig/stoplight/domain/ports/telemetry.rbs +28 -0
  146. data/sig/stoplight/domain/ports/telemetry_publisher.rbs +14 -0
  147. data/sig/{_private/stoplight → stoplight}/domain/ports/traffic_control.rbs +7 -0
  148. data/sig/{_private/stoplight → stoplight}/domain/ports/traffic_recovery.rbs +2 -3
  149. data/sig/stoplight/domain/strategies/green_run_strategy.rbs +22 -0
  150. data/sig/stoplight/domain/strategies/red_run_strategy.rbs +19 -0
  151. data/sig/{_private/stoplight → stoplight}/domain/strategies/yellow_run_strategy.rbs +23 -12
  152. data/sig/stoplight/domain/telemetry/bus.rbs +49 -0
  153. data/sig/stoplight/domain/telemetry/consumer.rbs +13 -0
  154. data/sig/stoplight/domain/telemetry/emitter.rbs +51 -0
  155. data/sig/stoplight/domain/telemetry/envelope.rbs +30 -0
  156. data/sig/stoplight/domain/telemetry/failure.rbs +23 -0
  157. data/sig/stoplight/domain/telemetry/light_registered.rbs +23 -0
  158. data/sig/stoplight/domain/telemetry/lock_changed.rbs +31 -0
  159. data/sig/stoplight/domain/telemetry/metrics.rbs +27 -0
  160. data/sig/stoplight/domain/telemetry/recovery_failed.rbs +36 -0
  161. data/sig/stoplight/domain/telemetry/recovery_probe_completed.rbs +29 -0
  162. data/sig/stoplight/domain/telemetry/recovery_started.rbs +26 -0
  163. data/sig/stoplight/domain/telemetry/recovery_succeeded.rbs +31 -0
  164. data/sig/stoplight/domain/telemetry/run_completed.rbs +38 -0
  165. data/sig/stoplight/domain/telemetry/run_recorder.rbs +27 -0
  166. data/sig/stoplight/domain/telemetry/settings.rbs +47 -0
  167. data/sig/stoplight/domain/telemetry/state_transitioned.rbs +11 -0
  168. data/sig/stoplight/domain/telemetry/subscription.rbs +9 -0
  169. data/sig/stoplight/domain/telemetry/traffic_breached.rbs +36 -0
  170. data/sig/stoplight/domain/telemetry/types.rbs +32 -0
  171. data/sig/{_private/stoplight → stoplight}/domain/tracker/recovery_probe.rbs +10 -6
  172. data/sig/{_private/stoplight → stoplight}/domain/tracker/request.rbs +3 -3
  173. data/sig/{_private/stoplight → stoplight}/domain/traffic_control/consecutive_errors.rbs +2 -0
  174. data/sig/{_private/stoplight → stoplight}/domain/traffic_control/error_rate.rbs +3 -2
  175. data/sig/{_private/stoplight → stoplight}/domain/traffic_recovery/consecutive_successes.rbs +2 -0
  176. data/sig/stoplight/domain/types.rbs +25 -0
  177. data/sig/stoplight/error.rbs +8 -0
  178. data/sig/stoplight/generators/rails_generators_base.rbs +11 -0
  179. data/sig/stoplight/infrastructure/config_serializer.rbs +12 -0
  180. data/sig/stoplight/infrastructure/fail_safe/error_notifier.rbs +13 -0
  181. data/sig/{_private/stoplight → stoplight}/infrastructure/fail_safe/storage/metrics.rbs +2 -2
  182. data/sig/{_private/stoplight → stoplight}/infrastructure/fail_safe/storage/recovery_lock.rbs +2 -2
  183. data/sig/stoplight/infrastructure/fail_safe/storage/registry.rbs +23 -0
  184. data/sig/{_private/stoplight → stoplight}/infrastructure/fail_safe/storage/state.rbs +4 -4
  185. data/sig/stoplight/infrastructure/memory/storage/registry.rbs +13 -0
  186. data/sig/{_private/stoplight → stoplight}/infrastructure/memory/storage/state.rbs +3 -3
  187. data/sig/{_private/stoplight → stoplight}/infrastructure/memory/storage/unbounded_metrics.rbs +7 -7
  188. data/sig/stoplight/infrastructure/memory/storage/window_metrics/sliding_window.rbs +27 -0
  189. data/sig/{_private/stoplight → stoplight}/infrastructure/memory/storage/window_metrics.rbs +7 -7
  190. data/sig/stoplight/infrastructure/notifier/fail_safe.rbs +15 -0
  191. data/sig/{_private/stoplight → stoplight}/infrastructure/notifier/generic.rbs +1 -1
  192. data/sig/{_private/stoplight → stoplight}/infrastructure/notifier/io.rbs +1 -1
  193. data/sig/{_private/stoplight → stoplight}/infrastructure/notifier/logger.rbs +1 -1
  194. data/sig/stoplight/infrastructure/redis/key.rbs +10 -0
  195. data/sig/{_private/stoplight → stoplight}/infrastructure/redis/storage/recovery_lock.rbs +2 -2
  196. data/sig/stoplight/infrastructure/redis/storage/registry.rbs +27 -0
  197. data/sig/stoplight/infrastructure/redis/storage/scripting.rbs +34 -0
  198. data/sig/{_private/stoplight → stoplight}/infrastructure/redis/storage/state.rbs +4 -4
  199. data/sig/{_private/stoplight → stoplight}/infrastructure/redis/storage/unbounded_metrics.rbs +8 -1
  200. data/sig/stoplight/infrastructure/redis/storage/window_metrics.rbs +35 -0
  201. data/sig/stoplight/infrastructure/types.rbs +6 -0
  202. data/sig/stoplight/notifier.rbs +1 -1
  203. data/sig/stoplight/ports/system.rbs +16 -10
  204. data/sig/stoplight/state.rbs +3 -3
  205. data/sig/stoplight/telemetry.rbs +17 -0
  206. data/sig/stoplight/types.rbs +9 -0
  207. data/sig/{_private/stoplight → stoplight}/wiring/config_compatibility_validator.rbs +2 -0
  208. data/sig/stoplight/wiring/default.rbs +26 -0
  209. data/sig/stoplight/wiring/default_configuration.rbs +39 -0
  210. data/sig/stoplight/wiring/external_caller.rbs +13 -0
  211. data/sig/stoplight/wiring/fail_safe_config.rbs +7 -0
  212. data/sig/stoplight/wiring/global_state.rbs +36 -0
  213. data/sig/{_private/stoplight/wiring/light_factory → stoplight/wiring/light_configuration_dsl}/traffic_control_dsl.rbs +1 -1
  214. data/sig/{_private/stoplight/wiring/light_factory → stoplight/wiring/light_configuration_dsl}/traffic_recovery_dsl.rbs +1 -1
  215. data/sig/stoplight/wiring/light_configuration_dsl.rbs +38 -0
  216. data/sig/stoplight/wiring/light_factory.rbs +78 -0
  217. data/sig/stoplight/wiring/notifier_bridge.rbs +15 -0
  218. data/sig/stoplight/wiring/notifier_factory.rbs +10 -0
  219. data/sig/{_private/stoplight → stoplight}/wiring/redis/backend.rbs +6 -6
  220. data/sig/stoplight/wiring/system/storage.rbs +35 -0
  221. data/sig/stoplight/wiring/system.rbs +30 -0
  222. data/sig/stoplight/wiring/system_configuration_dsl.rbs +39 -0
  223. data/sig/stoplight/wiring/types.rbs +6 -0
  224. data/sig/stoplight.rbs +37 -54
  225. metadata +190 -143
  226. data/lib/stoplight/admin/actions/lock_all_green.rb +0 -18
  227. data/lib/stoplight/admin/actions/lock_green.rb +0 -23
  228. data/lib/stoplight/admin/actions/lock_red.rb +0 -23
  229. data/lib/stoplight/admin/lights_repository/light.rb +0 -171
  230. data/lib/stoplight/admin/lights_repository.rb +0 -87
  231. data/lib/stoplight/domain/light/configuration_builder_interface.rb +0 -236
  232. data/lib/stoplight/infrastructure/fail_safe/data_store.rb +0 -152
  233. data/lib/stoplight/infrastructure/memory/data_store/metrics.rb +0 -27
  234. data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_store.rb +0 -52
  235. data/lib/stoplight/infrastructure/memory/data_store/recovery_lock_token.rb +0 -17
  236. data/lib/stoplight/infrastructure/memory/data_store/sliding_window.rb +0 -74
  237. data/lib/stoplight/infrastructure/memory/data_store/state.rb +0 -21
  238. data/lib/stoplight/infrastructure/memory/data_store.rb +0 -342
  239. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/get_metrics.lua +0 -26
  240. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_failure.lua +0 -36
  241. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_failure.lua +0 -27
  242. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_recovery_probe_success.lua +0 -23
  243. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/record_success.lua +0 -35
  244. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/release_lock.lua +0 -6
  245. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/transition_to_green.lua +0 -10
  246. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/transition_to_red.lua +0 -10
  247. data/lib/stoplight/infrastructure/redis/data_store/lua_scripts/transition_to_yellow.lua +0 -9
  248. data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_store.rb +0 -60
  249. data/lib/stoplight/infrastructure/redis/data_store/recovery_lock_token.rb +0 -28
  250. data/lib/stoplight/infrastructure/redis/data_store/scripting.rb +0 -73
  251. data/lib/stoplight/infrastructure/redis/data_store.rb +0 -516
  252. data/lib/stoplight/infrastructure/redis/storage/key_space.rb +0 -51
  253. data/lib/stoplight/infrastructure/storage/compatibility_metrics.rb +0 -41
  254. data/lib/stoplight/infrastructure/storage/compatibility_recovery_lock.rb +0 -33
  255. data/lib/stoplight/infrastructure/storage/compatibility_recovery_metrics.rb +0 -47
  256. data/lib/stoplight/infrastructure/storage/compatibility_state.rb +0 -44
  257. data/lib/stoplight/wiring/light_builder.rb +0 -198
  258. data/lib/stoplight/wiring/notifier_factory.rb +0 -26
  259. data/lib/stoplight/wiring/system/light_builder.rb +0 -47
  260. data/lib/stoplight/wiring/system/light_factory.rb +0 -64
  261. data/sig/_private/stoplight/domain/light.rbs +0 -25
  262. data/sig/_private/stoplight/domain/ports/data_store.rbs +0 -76
  263. data/sig/_private/stoplight/domain/ports/light_factory.rbs +0 -80
  264. data/sig/_private/stoplight/domain/strategies/green_run_strategy.rbs +0 -17
  265. data/sig/_private/stoplight/domain/strategies/red_run_strategy.rbs +0 -17
  266. data/sig/_private/stoplight/domain/tracker/base.rbs +0 -8
  267. data/sig/_private/stoplight/infrastructure/fail_safe/data_store.rbs +0 -26
  268. data/sig/_private/stoplight/infrastructure/memory/data_store/metrics.rbs +0 -25
  269. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_store.rbs +0 -19
  270. data/sig/_private/stoplight/infrastructure/memory/data_store/recovery_lock_token.rbs +0 -17
  271. data/sig/_private/stoplight/infrastructure/memory/data_store/sliding_window.rbs +0 -27
  272. data/sig/_private/stoplight/infrastructure/memory/data_store/state.rbs +0 -17
  273. data/sig/_private/stoplight/infrastructure/memory/data_store.rbs +0 -30
  274. data/sig/_private/stoplight/infrastructure/notifier/fail_safe.rbs +0 -17
  275. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_store.rbs +0 -24
  276. data/sig/_private/stoplight/infrastructure/redis/data_store/recovery_lock_token.rbs +0 -21
  277. data/sig/_private/stoplight/infrastructure/redis/data_store/scripting.rbs +0 -34
  278. data/sig/_private/stoplight/infrastructure/redis/data_store.rbs +0 -67
  279. data/sig/_private/stoplight/infrastructure/redis/storage/key_space.rbs +0 -19
  280. data/sig/_private/stoplight/infrastructure/redis/storage/scripting.rbs +0 -13
  281. data/sig/_private/stoplight/infrastructure/redis/storage/window_metrics.rbs +0 -34
  282. data/sig/_private/stoplight/infrastructure/storage/compatibility_metrics.rbs +0 -17
  283. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_lock.rbs +0 -13
  284. data/sig/_private/stoplight/infrastructure/storage/compatibility_recovery_metrics.rbs +0 -14
  285. data/sig/_private/stoplight/infrastructure/storage/compatibility_state.rbs +0 -14
  286. data/sig/_private/stoplight/system/light_builder.rbs +0 -23
  287. data/sig/_private/stoplight/system/light_factory.rbs +0 -17
  288. data/sig/_private/stoplight/types.rbs +0 -6
  289. data/sig/_private/stoplight/wiring/configuration_dsl.rbs +0 -43
  290. data/sig/_private/stoplight/wiring/default.rbs +0 -26
  291. data/sig/_private/stoplight/wiring/default_configuration.rbs +0 -29
  292. data/sig/_private/stoplight/wiring/light_builder.rbs +0 -48
  293. data/sig/_private/stoplight/wiring/light_factory.rbs +0 -16
  294. data/sig/_private/stoplight/wiring/notifier_factory.rbs +0 -10
  295. data/sig/_private/stoplight/wiring/system.rbs +0 -15
  296. data/sig/_private/stoplight.rbs +0 -48
  297. data/sig/stoplight/data_store.rbs +0 -19
  298. data/sig/stoplight/ports/configuration.rbs +0 -19
  299. data/sig/stoplight/ports/exception_matcher.rbs +0 -8
  300. data/sig/stoplight/ports/light.rbs +0 -12
  301. data/sig/stoplight/ports/light_info.rbs +0 -5
  302. data/sig/stoplight/ports/state_transition_notifier.rbs +0 -15
  303. /data/sig/{_private/stoplight → stoplight}/common/deprecations.rbs +0 -0
  304. /data/sig/{_private/stoplight → stoplight}/domain/compatibility_result.rbs +0 -0
  305. /data/sig/{_private/stoplight → stoplight}/domain/metrics_snapshot.rbs +0 -0
  306. /data/sig/{_private/stoplight → stoplight}/domain/ports/recovery_lock_store.rbs +0 -0
  307. /data/sig/{_private/stoplight → stoplight}/domain/ports/recovery_lock_token.rbs +0 -0
  308. /data/sig/{_private/stoplight → stoplight}/domain/ports/state_store.rbs +0 -0
  309. /data/sig/{_private/stoplight → stoplight}/domain/state_snapshot.rbs +0 -0
  310. /data/sig/{_private/stoplight → stoplight}/domain/storage/recovery_lock_token.rbs +0 -0
  311. /data/sig/{_private/stoplight → stoplight}/domain/traffic_recovery.rbs +0 -0
  312. /data/sig/{_private → stoplight}/generators/stoplight/install/install_generator.rbs +0 -0
  313. /data/sig/{_private/stoplight → stoplight}/infrastructure/fail_safe/storage/recovery_lock_token.rbs +0 -0
  314. /data/sig/{_private/stoplight → stoplight}/infrastructure/memory/storage/recovery_lock.rbs +0 -0
  315. /data/sig/{_private/stoplight → stoplight}/infrastructure/memory/storage/recovery_metrics.rbs +0 -0
  316. /data/sig/{_private/stoplight → stoplight}/infrastructure/redis/storage/metrics.rbs +0 -0
  317. /data/sig/{_private/stoplight → stoplight}/infrastructure/redis/storage/recovery_metrics.rbs +0 -0
  318. /data/sig/{_private/stoplight → stoplight}/infrastructure/system_clock.rbs +0 -0
  319. /data/sig/{_private/stoplight → stoplight}/wiring/data_store_backend.rbs +0 -0
  320. /data/sig/{_private/stoplight → stoplight}/wiring/default_config.rbs +0 -0
  321. /data/sig/{_private/stoplight → stoplight}/wiring/memory/backend.rbs +0 -0
  322. /data/sig/{_private/stoplight → stoplight}/wiring/storage_set.rbs +0 -0
  323. /data/sig/{_private/stoplight → stoplight}/wiring/storage_set_builder.rbs +0 -0
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module Stoplight
6
+ class Admin
7
+ class LightView
8
+ COLORS = [
9
+ Stoplight::Color::GREEN,
10
+ Stoplight::Color::YELLOW,
11
+ Stoplight::Color::RED
12
+ ].freeze
13
+
14
+ attr_reader :failures
15
+
16
+ def initialize(
17
+ config:,
18
+ state_snapshot:,
19
+ metrics_snapshot:,
20
+ recovery_metrics_snapshot:
21
+ )
22
+ @config = config
23
+ @state_snapshot = state_snapshot
24
+ @recovery_metrics_snapshot = recovery_metrics_snapshot
25
+ @metrics_snapshot = metrics_snapshot
26
+ @failures = [metrics_snapshot.last_error].compact
27
+ end
28
+
29
+ def locked?
30
+ !unlocked?
31
+ end
32
+
33
+ def color = @state_snapshot.color
34
+ def id = @config.id
35
+ def name = @config.name
36
+ def state = @state_snapshot.locked_state
37
+ def latest_failure = @metrics_snapshot.last_error
38
+
39
+ def traffic_metric_label
40
+ if @metrics_snapshot.requests.nil?
41
+ "Consecutive failures"
42
+ else
43
+ "Errors"
44
+ end
45
+ end
46
+
47
+ def traffic_metric_value
48
+ requests = @metrics_snapshot.requests
49
+ if requests.nil?
50
+ @metrics_snapshot.consecutive_errors.to_s
51
+ else
52
+ error_rate = @metrics_snapshot.error_rate
53
+ raise TypeError, "error_rate must not be nil" if error_rate.nil?
54
+
55
+ "#{@metrics_snapshot.errors!} / #{requests} requests (#{format("%.1f%%", error_rate * 100)})"
56
+ end
57
+ end
58
+
59
+ def unlocked?
60
+ @state_snapshot.locked_state == Stoplight::State::UNLOCKED
61
+ end
62
+
63
+ # @return [Hash]
64
+ def as_json
65
+ {
66
+ id: @config.id,
67
+ name: @config.name,
68
+ color: @state_snapshot.color,
69
+ failures: Array(@metrics_snapshot.last_error),
70
+ locked: locked?
71
+ }
72
+ end
73
+
74
+ # @return [Array]
75
+ def default_sort_key
76
+ [-COLORS.index(@state_snapshot.color).to_i, @config.name]
77
+ end
78
+
79
+ def last_check = [
80
+ @metrics_snapshot.last_error_at,
81
+ @metrics_snapshot.last_success_at,
82
+ @recovery_metrics_snapshot.last_error_at,
83
+ @recovery_metrics_snapshot.last_success_at,
84
+ @state_snapshot.breached_at
85
+ ].compact.max
86
+
87
+ private def duration_in_words(seconds)
88
+ minutes = seconds / 60
89
+ seconds %= 60
90
+ hours = minutes / 60
91
+ minutes %= 60
92
+ parts = [] #: Array[String]
93
+ parts << "#{hours} #{(hours == 1) ? "hour" : "hours"}" if hours > 0
94
+ parts << "#{minutes} #{(minutes == 1) ? "minute" : "minutes"}" if minutes > 0
95
+ parts << "#{seconds} #{(seconds == 1) ? "second" : "seconds"}" if seconds > 0
96
+ parts.join(", ")
97
+ end
98
+
99
+ # @return [String]
100
+ def description_title
101
+ case @state_snapshot.color
102
+ when Stoplight::Color::RED
103
+ if locked? && @metrics_snapshot.last_error.nil?
104
+ "Locked Open"
105
+ else
106
+ "Last Error"
107
+ end
108
+ when Stoplight::Color::YELLOW
109
+ "Testing Recovery"
110
+ when Stoplight::Color::GREEN
111
+ if locked?
112
+ "Forced Healthy"
113
+ else
114
+ "Healthy"
115
+ end
116
+ else
117
+ raise T.absurd
118
+ end
119
+ end
120
+
121
+ # @return [String]
122
+ def description_message
123
+ case @state_snapshot.color
124
+ when Stoplight::Color::RED
125
+ if (latest_failure = @metrics_snapshot.last_error)
126
+ "#{latest_failure.error_class}: #{latest_failure.error_message}"
127
+ elsif locked?
128
+ "Circuit manually locked open"
129
+ else
130
+ "Not available"
131
+ end
132
+ when Stoplight::Color::YELLOW
133
+ if (latest_failure = @metrics_snapshot.last_error)
134
+ "#{latest_failure.error_class}: #{latest_failure.error_message}"
135
+ else
136
+ "Not available"
137
+ end
138
+ when Stoplight::Color::GREEN
139
+ if locked?
140
+ "Circuit manually locked closed"
141
+ else
142
+ "No recent errors"
143
+ end
144
+ else
145
+ raise T.absurd
146
+ end
147
+ end
148
+
149
+ # @return [String]
150
+ def description_comment
151
+ case @state_snapshot.color
152
+ when Stoplight::Color::RED
153
+ if locked?
154
+ "Override active - all requests blocked"
155
+ elsif (recovery_at = @state_snapshot.recovery_scheduled_after)
156
+ recovery_in = [0, (recovery_at - Time.now.utc).to_i].max
157
+ if recovery_in > 0
158
+ "Will attempt recovery in #{duration_in_words(recovery_in)}"
159
+ else
160
+ "Recovery started: awaiting test traffic"
161
+ end
162
+ else
163
+ "Recovery started: awaiting test traffic"
164
+ end
165
+ when Stoplight::Color::YELLOW
166
+ recovery_metrics_snapshot = @recovery_metrics_snapshot
167
+ if recovery_metrics_snapshot.consecutive_successes.zero?
168
+ "Recovery started: awaiting test traffic"
169
+ else
170
+ "Allowing limited test traffic (#{recovery_metrics_snapshot.consecutive_successes} of #{@config.recovery_threshold} requests)"
171
+ end
172
+ when Stoplight::Color::GREEN
173
+ if locked?
174
+ "Override active - all requests processed"
175
+ else
176
+ "Operating normally"
177
+ end
178
+ else
179
+ raise T.absurd
180
+ end
181
+ end
182
+ end
183
+ end
184
+ end
@@ -8,8 +8,6 @@ module Stoplight
8
8
  percent_red: 0, percent_yellow: 0, percent_green: 0
9
9
  }.freeze
10
10
 
11
- # @!attribute lights
12
- # @return [<Stoplight::Admin::LightsRepository::Light>]
13
11
  attr_reader :lights
14
12
  private :lights
15
13
 
@@ -19,7 +17,7 @@ module Stoplight
19
17
  end
20
18
  end
21
19
 
22
- # @param lights [<Stoplight::Admin::LightsRepository::Light>]
20
+ # @param lights [<Stoplight::Admin::LightView>]
23
21
  def initialize(lights)
24
22
  @lights = lights
25
23
  end
@@ -38,27 +36,27 @@ module Stoplight
38
36
  end
39
37
 
40
38
  private def count_red
41
- count_lights(RED)
39
+ count_lights(Color::RED)
42
40
  end
43
41
 
44
42
  private def percent_red
45
- percent_lights(RED)
43
+ percent_lights(Color::RED)
46
44
  end
47
45
 
48
46
  private def count_green
49
- count_lights(GREEN)
47
+ count_lights(Color::GREEN)
50
48
  end
51
49
 
52
50
  private def percent_green
53
- percent_lights(GREEN)
51
+ percent_lights(Color::GREEN)
54
52
  end
55
53
 
56
54
  private def count_yellow
57
- count_lights(YELLOW)
55
+ count_lights(Color::YELLOW)
58
56
  end
59
57
 
60
58
  private def percent_yellow
61
- percent_lights(YELLOW)
59
+ percent_lights(Color::YELLOW)
62
60
  end
63
61
 
64
62
  private def count_lights(color)
@@ -1,6 +1,6 @@
1
- <div class="max-w-xl mr-5 p-6 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
1
+ <div class="flex flex-col max-w-xl mr-5 p-6 border border-gray-200 rounded-lg shadow-sm dark:bg-gray-800 dark:border-gray-700">
2
2
  <div class="flex items-center">
3
- <% light_name = CGI.escape(light.name) %>
3
+ <% light_id = light.id %>
4
4
 
5
5
  <div class="relative">
6
6
  <div class="relative inline-flex items-center justify-center w-10 h-10 overflow-hidden bg-<%= color %>-100 rounded-full dark:bg-<%= color %>-600">
@@ -24,7 +24,7 @@
24
24
 
25
25
  <div class="flex-1 min-w-0 ms-4">
26
26
  <p class="text-sm font-medium text-gray-900 truncate dark:text-white">
27
- <%= light.name %>
27
+ <%= CGI.escapeHTML(light.name) %>
28
28
  </p>
29
29
  <span class="flex items-center text-sm font-medium text-gray-500 dark:text-gray-400 truncate me-3">
30
30
  <span class="flex w-2.5 h-2.5 bg-<%= color %>-600 rounded-full me-1.5 shrink-0"></span>
@@ -50,11 +50,13 @@
50
50
  </svg>
51
51
  </button>
52
52
 
53
+ <% action_classes = "flex items-center py-2 px-4 text-sm " + (settings.read_only? ? "text-gray-400 dark:text-gray-500 cursor-not-allowed" : "hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white") %>
54
+
53
55
  <!-- Dropdown menu -->
54
56
  <div id="dropdownDotsHorizontal-<%= light.id %>" class="z-10 hidden bg-white divide-y divide-gray-100 rounded-lg shadow-sm w-44 dark:bg-gray-700 dark:divide-gray-600">
55
57
  <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="dropdownMenuIconHorizontalButton">
56
58
  <li>
57
- <a href="<%= url("/unlock?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
59
+ <a <%= control_attributes(system_url(system_id, "/lights/#{light_id}/unlock"), verb: "patch") %> class="<%= action_classes %>">
58
60
  <svg class="flex w-4 h-4 me-1.5 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
59
61
  <circle cx="12" cy="16" r="1"/>
60
62
  <rect width="18" height="12" x="3" y="10" rx="2"/>
@@ -64,7 +66,7 @@
64
66
  </a>
65
67
  </li>
66
68
  <li>
67
- <a href="<%= url("/red?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
69
+ <a <%= control_attributes(system_url(system_id, "/lights/#{light_id}/lock?color=red"), verb: "patch") %> class="<%= action_classes %>">
68
70
  <svg class="flex w-4 h-4 me-1.5 text-red-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
69
71
  <circle cx="12" cy="16" r="1"/>
70
72
  <rect x="3" y="10" width="18" height="12" rx="2"/>
@@ -74,7 +76,7 @@
74
76
  </a>
75
77
  </li>
76
78
  <li>
77
- <a href="<%= url("/green?names=#{light_name}") %>" data-turbo-method="post" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
79
+ <a <%= control_attributes(system_url(system_id, "/lights/#{light_id}/lock?color=green"), verb: "patch") %> class="<%= action_classes %>">
78
80
  <svg class="flex w-4 h-4 me-1.5 text-green-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
79
81
  <circle cx="12" cy="16" r="1"/>
80
82
  <rect x="3" y="10" width="18" height="12" rx="2"/>
@@ -84,7 +86,7 @@
84
86
  </a>
85
87
  </li>
86
88
  <li>
87
- <a href="<%= url("/remove?names=#{light_name}") %>" data-turbo-method="post" data-turbo-confirm="Are you sure you want to remove this light?" class="flex items-center py-2 px-4 text-sm hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white">
89
+ <a <%= control_attributes(system_url(system_id, "/lights/#{light_id}"), confirm: "Are you sure you want to remove this light?", verb: "delete") %> class="<%= action_classes %>">
88
90
  <svg class="flex w-4 h-4 me-1.5 text-red-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
89
91
  <polyline points="3 6 5 6 21 6"/>
90
92
  <path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/>
@@ -101,13 +103,9 @@
101
103
 
102
104
  <div class="p-4 my-4 text-sm text-<%= color %>-800 rounded-lg bg-<%= color %>-50 dark:bg-gray-800 dark:text-<%= color %>-400">
103
105
  <div class="flex justify-between items-center">
104
- <div class="pr-4">
105
- <p class="font-semibold"><%= light.description_title %></p>
106
- <p class="font-medium"><%= light.description_message %></p>
107
- <p><%= light.description_comment %></p>
108
- </div>
106
+ <p class="font-semibold pr-4"><%= CGI.escapeHTML(light.description_title) %></p>
109
107
  <% if light.latest_failure %>
110
- <div class="whitespace-nowrap text-center">
108
+ <div class="whitespace-nowrap text-center shrink-0">
111
109
  <%= light.latest_failure.time.strftime("%F %T") %>
112
110
  <span class="text-gray-400 dark:text-gray-500 uppercase tracking-wide">
113
111
  UTC
@@ -115,20 +113,20 @@
115
113
  </div>
116
114
  <% end %>
117
115
  </div>
116
+ <p class="font-medium"><%= CGI.escapeHTML(light.description_message) %></p>
117
+ <p><%= CGI.escapeHTML(light.description_comment) %></p>
118
118
  </div>
119
119
 
120
120
  <!-- Stats Row -->
121
- <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-3">
121
+ <div class="flex justify-between text-xs text-gray-500 dark:text-gray-400 mt-3" style="margin-top: auto;">
122
122
  <div>
123
- <span class="font-medium">Failures:</span> <%= light.failure_count %>
123
+ <span class="font-medium"><%= light.traffic_metric_label %>:</span> <%= light.traffic_metric_value %>
124
124
  </div>
125
125
 
126
- <% light.last_check.then do |last_check| %>
127
- <% if last_check %>
128
- <div>
129
- <span class="font-medium">Last Check:</span><%= time_ago_in_words(last_check) %>
130
- </div>
131
- <% end %>
126
+ <% if light.last_check %>
127
+ <div>
128
+ <span class="font-medium">Last Check:</span><%= time_ago_in_words(light.last_check) %>
129
+ </div>
132
130
  <% end %>
133
131
  </div>
134
132
  </div>
@@ -13,7 +13,7 @@
13
13
 
14
14
  <div class="space-y-4">
15
15
  <p class="mb-8 text-lg font-normal text-gray-500 lg:text-xl sm:px-16 lg:px-48 dark:text-gray-400">
16
- Ensure that your Stoplight data store is properly configured and that your Stoplight blocks have been run.
16
+ Ensure lights are registered and this Admin instance uses the same data store as your application.
17
17
  </p>
18
18
  </div>
19
19
 
@@ -28,9 +28,15 @@
28
28
  </div>
29
29
  </section>
30
30
  <% else %>
31
- <section class="grid gap-4" style="grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));">
31
+ <style nonce="<%= nonce %>">
32
+ .lights-grid { grid-template-columns: 1fr; }
33
+ @media (min-width: 800px) {
34
+ .lights-grid { grid-template-columns: repeat(auto-fill, 500px); }
35
+ }
36
+ </style>
37
+ <section class="grid gap-4 lights-grid">
32
38
  <% lights.each do |light| %>
33
- <%= erb :_card, locals: { light:, color: light.color } %>
39
+ <%= erb :_card, locals: { light:, color: light.color, system_id: } %>
34
40
  <% end %>
35
41
  </section>
36
42
  <% end %>
@@ -6,23 +6,33 @@
6
6
 
7
7
  <title>Stoplight Admin</title>
8
8
 
9
- <script nonce="<%= nonce %>" type="module" src="https://cdn.jsdelivr.net/npm/@hotwired/turbo@latest/dist/turbo.es2017-esm.min.js"></script>
9
+ <link rel="icon" href="<%= asset_path('favicon.ico') %>" sizes="32x32">
10
+ <link rel="icon" href="<%= asset_path('icon.svg') %>" type="image/svg+xml">
11
+ <link rel="apple-touch-icon" href="<%= asset_path('apple-touch-icon.png') %>">
12
+
13
+ <script nonce="<%= nonce %>" type="module" src="<%= asset_path('turbo.es2017-esm.min.js') %>"></script>
10
14
  <script nonce="<%= nonce %>" type="module">
11
15
  document.addEventListener("turbo:load", () => {
12
16
  window.initFlowbite()
13
17
  })
14
18
  </script>
15
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.css" />
19
+ <link rel="stylesheet" href="<%= asset_path('flowbite.min.css') %>" />
16
20
  </head>
17
- <body>
18
- <div class="antialiased bg-gray-50 dark:bg-gray-900 h-screen flex flex-col justify-between">
19
- <div>
21
+ <body class="bg-gray-50 dark:bg-gray-900">
22
+ <div class="antialiased bg-gray-50 dark:bg-gray-900 flex flex-col" style="min-height: 100vh;">
23
+ <div class="grow">
20
24
  <nav class="bg-white border-gray-200 dark:bg-gray-900">
21
- <div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
22
- <a href="<%= url('/') %>" class="flex items-center space-x-3 rtl:space-x-reverse">
23
- 🚦
24
- <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Stoplight Admin</span>
25
- </a>
25
+ <div class="flex flex-wrap items-center justify-between p-4">
26
+ <div class="flex items-center space-x-3 rtl:space-x-reverse">
27
+ <a href="<%= url('/') %>" class="flex items-center space-x-3 rtl:space-x-reverse">
28
+ 🚦
29
+ <span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">Stoplight Admin</span>
30
+ </a>
31
+
32
+ <% if settings.read_only? %>
33
+ <span class="bg-gray-100 text-gray-600 text-xs font-medium px-2.5 py-0.5 rounded-sm dark:bg-gray-700 dark:text-gray-300" title="This panel cannot change light states">Read-only</span>
34
+ <% end %>
35
+ </div>
26
36
 
27
37
  <button data-collapse-toggle="navbar-default" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" aria-controls="navbar-default" aria-expanded="false">
28
38
  <span class="sr-only">Open main menu</span>
@@ -33,9 +43,31 @@
33
43
 
34
44
  <div class="hidden w-full md:block md:w-auto" id="navbar-default">
35
45
  <ul class="font-medium flex flex-col p-4 md:p-0 mt-4 border border-gray-100 rounded-lg bg-gray-50 md:flex-row md:space-x-8 rtl:space-x-reverse md:mt-0 md:border-0 md:bg-white dark:bg-gray-800 md:dark:bg-gray-900 dark:border-gray-700">
46
+ <% if show_system_switcher? %>
47
+ <li class="relative me-2 mb-2">
48
+ <button id="systemMenuButton" data-dropdown-toggle="systemMenu" type="button"
49
+ class="flex items-center gap-2 py-2.5 px-4 text-sm font-medium text-gray-900 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 focus:ring-4 focus:outline-none focus:ring-gray-100 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:focus:ring-gray-700">
50
+ <%= CGI.escapeHTML(current_system.name) %>
51
+ <svg class="w-2.5 h-2.5 ms-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 6" fill="none">
52
+ <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 4 4 4-4"/>
53
+ </svg>
54
+ </button>
55
+ <div id="systemMenu" class="z-10 hidden w-60 bg-white divide-y divide-gray-100 rounded-lg shadow-lg border border-gray-100 dark:bg-gray-700 dark:divide-gray-600 dark:border-gray-600">
56
+ <ul class="py-2 text-sm text-gray-700 dark:text-gray-200" aria-labelledby="systemMenuButton">
57
+ <% settings.systems.each do |other_system| %>
58
+ <li>
59
+ <a href="<%= system_url(other_system.config.id, "/lights") %>" class="block px-4 py-2 <%= (other_system.config.id == current_system_id) ? "bg-blue-50 text-blue-700 dark:bg-gray-600 dark:text-white" : "hover:bg-gray-100 dark:hover:bg-gray-600 dark:hover:text-white" %>">
60
+ <%= CGI.escapeHTML(other_system.name) %>
61
+ </a>
62
+ </li>
63
+ <% end %>
64
+ </ul>
65
+ </div>
66
+ </li>
67
+ <% end %>
36
68
  <% if count_red + count_yellow > 0 %>
37
69
  <li>
38
- <a href="<%= url('/green_all') %>" data-turbo-method="post" class="inline-flex items-center text-gray-900 bg-white border border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-gray-800 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700">
70
+ <a <%= control_attributes(system_url(system_id, '/lights/lock?color=green'), verb: "patch") %> class="inline-flex items-center font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 border bg-white dark:bg-gray-800 <%= settings.read_only? ? "text-gray-400 border-gray-200 cursor-not-allowed dark:text-gray-500 dark:border-gray-700" : "text-gray-900 border-gray-300 focus:outline-none hover:bg-gray-100 focus:ring-4 focus:ring-gray-100 dark:text-white dark:border-gray-600 dark:hover:bg-gray-700 dark:hover:border-gray-600 dark:focus:ring-gray-700" %>">
39
71
  <svg class="flex w-4 h-4 me-1.5 text-green-600 shrink-0" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
40
72
  <circle cx="12" cy="16" r="1"/>
41
73
  <rect x="3" y="10" width="18" height="12" rx="2"/>
@@ -57,12 +89,12 @@
57
89
 
58
90
  <footer class="bg-white rounded-lg shadow-sm dark:bg-gray-900 m-4">
59
91
  <div class="w-full max-w-screen-xl mx-auto p-4 md:py-8">
60
- <p class="block text-sm text-gray-500 sm:text-center dark:text-gray-400 mb-2">All of the Lights -- cop lights, flashlights, spotlights, strobe lights...</p>
92
+ <p class="block text-sm text-gray-500 sm:text-center dark:text-gray-400 mb-2">All of the Lights cop lights, flashlights, spotlights, strobe lights…</p>
61
93
  <p class="block text-sm text-gray-500 sm:text-center dark:text-gray-400">💡 🔦 🚨 🚥 💫</p>
62
94
  </div>
63
95
  </footer>
64
96
  </div>
65
97
 
66
- <script nonce="<%= nonce %>" src="https://cdn.jsdelivr.net/npm/flowbite@3.1.2/dist/flowbite.min.js"></script>
98
+ <script nonce="<%= nonce %>" src="<%= asset_path('flowbite.min.js') %>"></script>
67
99
  </body>
68
100
  </html>
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "cgi/escape"
4
4
  require "cgi/util" if RUBY_VERSION < "3.5"
5
+ require "digest"
5
6
 
6
7
  begin
7
8
  require "sinatra/base"
@@ -16,61 +17,149 @@ rescue LoadError
16
17
  end
17
18
 
18
19
  module Stoplight
20
+ # A Sinatra dashboard listing every light, its color, and its recent failures, with
21
+ # controls to lock a light green or red, unlock it, or remove it.
22
+ #
23
+ # Setting +read_only+ deploys it as an observation-only dashboard: every light stays
24
+ # visible, and every control that would change one is refused with 403 rather than merely
25
+ # hidden, so calling the endpoints directly does not get around it. The standalone Docker
26
+ # image reads the same option from +STOPLIGHT_ADMIN_READ_ONLY+, for the exact string +true+.
27
+ #
28
+ # This is not access control - it has no notion of users, and anyone who reaches the panel
29
+ # still reads every light name and failure message. Keep it behind authentication either way.
30
+ #
31
+ # @example Observing without being able to change anything
32
+ # Stoplightt::Admin.conrfigure do |config|
33
+ # config.read_only = true
34
+ # end
35
+ #
36
+ # mount Stoplight::Admin => "/stoplights"
37
+ #
38
+ # @example configue multiple systems
39
+ # Core = Stoplight.register_system("Core", data_store:)
40
+ # Analytics = Stoplight.register_system("Analytics", data_store:)
41
+ #
42
+ # Stoplight::Admin.configure do |config|
43
+ # config.add_system Core
44
+ # config.add_system Analytics
45
+ # end
46
+ #
19
47
  class Admin < Sinatra::Base
20
48
  COLORS = [
21
- GREEN = Stoplight::Color::GREEN,
22
- YELLOW = Stoplight::Color::YELLOW,
23
- RED = Stoplight::Color::RED
49
+ Color::GREEN,
50
+ Color::YELLOW,
51
+ Color::RED
24
52
  ].freeze
25
53
  private_constant :COLORS
26
54
 
55
+ ASSETS_PATH = File.join(T.must(__dir__), "admin", "assets")
56
+ ASSET_DIGESTS = Dir.children(ASSETS_PATH).to_h do |name|
57
+ [name, T.must(Digest::SHA256.file(File.join(ASSETS_PATH, name)).hexdigest[0, 8])]
58
+ end.freeze
59
+
60
+ ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
61
+ private_constant :ONE_YEAR_IN_SECONDS
62
+
27
63
  helpers Helpers
64
+ @systems = []
65
+
66
+ def self.add_system(system)
67
+ @systems << validate_persistent_system!(system)
68
+ end
69
+
70
+ def self.__stoplight__reset_systems!
71
+ @systems = []
72
+ end
73
+
74
+ def self.validate_persistent_system!(system)
75
+ return system if system.persistent?
76
+
77
+ raise TypeError, "Stoplight Admin requires a persistent data store, but the current data store is not persistent. " \
78
+ "Please configure a different data store in your Stoplight configuration."
79
+ end
80
+ private_class_method :validate_persistent_system!
81
+
82
+ set :systems do
83
+ if @systems.empty?
84
+ [validate_persistent_system!(Stoplight.__stoplight__default_system)]
85
+ else
86
+ @systems
87
+ end
88
+ end
28
89
 
29
90
  set :protection, except: %i[json_csrf]
91
+ set :read_only, false
30
92
  set :data_store, proc { Stoplight.__stoplight__default_configuration.data_store }
31
- set :views, File.join(__dir__, "admin", "views")
93
+ set :views, File.join(T.must(__dir__), "admin", "views")
32
94
  set :nonce, proc { |request| }
95
+ set :public_folder, ASSETS_PATH
96
+ set :static_cache_control, [:public, max_age: ONE_YEAR_IN_SECONDS, immutable: true]
97
+
98
+ # Gates on the request method rather than on a list of paths, so a write route added
99
+ # later is refused without anyone having to remember this filter exists.
100
+ before do
101
+ if settings.read_only? && !request.get? && !request.head?
102
+ halt 403, "Stoplight Admin is running in read-only mode."
103
+ end
104
+ end
33
105
 
34
106
  get "/" do
107
+ system = settings.systems.first
108
+
109
+ redirect system_url(system.config.id, "/lights")
110
+ end
111
+
112
+ get "/systems/:system_id/lights" do
35
113
  lights, stats = dependencies.stats_action.call
36
114
 
37
- erb :index, locals: stats.merge(lights: lights, nonce: settings.nonce(request))
115
+ erb :index, locals: stats.merge(
116
+ lights: lights,
117
+ nonce: settings.nonce(request),
118
+ system_id: current_system_id
119
+ )
38
120
  end
39
121
 
122
+ # Always reports the first configured system, regardless of which system is "current" -
123
+ # for external monitors that already poll this unscoped path.
40
124
  get "/stats" do
41
- lights, stats = dependencies.stats_action.call
125
+ lights, stats = Dependencies.new(system: settings.systems.first).stats_action.call
42
126
 
43
127
  json({stats: stats, lights: lights.map(&:as_json)})
44
128
  end
45
129
 
46
- post "/unlock" do
47
- dependencies.unlock_action.call(params)
130
+ get "/systems/:system_id/lights.json" do
131
+ lights, stats = dependencies.stats_action.call
48
132
 
49
- redirect to("/")
133
+ json({stats: stats, lights: lights.map(&:as_json)})
50
134
  end
51
135
 
52
- post "/green" do
53
- dependencies.green_action.call(params)
136
+ patch "/systems/:system_id/lights/:light_id/unlock" do
137
+ light_id = T.must(params[:light_id])
138
+ dependencies.unlock_action.call(light_id:)
54
139
 
55
- redirect to("/")
140
+ redirect system_url(current_system_id, "/lights")
56
141
  end
57
142
 
58
- post "/red" do
59
- dependencies.red_action.call(params)
143
+ patch "/systems/:system_id/lights/:light_id/lock" do
144
+ light_id = T.must(params[:light_id])
145
+ color = T.must(params[:color])
146
+ dependencies.lock_action.call(light_id:, color:)
60
147
 
61
- redirect to("/")
148
+ redirect system_url(current_system_id, "/lights")
62
149
  end
63
150
 
64
- post "/green_all" do
65
- dependencies.green_all_action.call
151
+ patch "/systems/:system_id/lights/lock" do
152
+ color = T.must(params[:color])
153
+ dependencies.lock_all_action.call(color:)
66
154
 
67
- redirect to("/")
155
+ redirect system_url(current_system_id, "/lights")
68
156
  end
69
157
 
70
- post "/remove" do
71
- dependencies.remove_action.call(params)
158
+ delete "/systems/:system_id/lights/:light_id" do
159
+ light_id = T.must(params[:light_id])
160
+ dependencies.remove_action.call(light_id:)
72
161
 
73
- redirect to("/")
162
+ redirect system_url(current_system_id, "/lights")
74
163
  end
75
164
  end
76
165
  end