logster 2.20.1 → 3.0.0

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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -92
  3. data/README.md +29 -14
  4. data/assets/javascript/main-B82IoCiW.js +1 -0
  5. data/assets/javascript/modules-4-12-BHA4lR2U.js +1 -0
  6. data/assets/javascript/vendor.js +11 -5260
  7. data/assets/logster-config.json +18 -0
  8. data/assets/manifest.json +23 -0
  9. data/assets/stylesheets/main-CgAFFyi8.css +1 -0
  10. data/lib/logster/configuration.rb +1 -0
  11. data/lib/logster/group.rb +2 -2
  12. data/lib/logster/message.rb +4 -4
  13. data/lib/logster/middleware/reporter.rb +34 -3
  14. data/lib/logster/middleware/viewer.rb +205 -33
  15. data/lib/logster/rails/railtie.rb +4 -20
  16. data/lib/logster/redis_store.rb +0 -2
  17. data/lib/logster/version.rb +1 -1
  18. data/lib/logster.rb +1 -1
  19. metadata +17 -186
  20. data/.github/workflows/ci.yml +0 -140
  21. data/.gitignore +0 -3
  22. data/.rubocop.yml +0 -8
  23. data/.streerc +0 -2
  24. data/Gemfile +0 -10
  25. data/Guardfile +0 -10
  26. data/Rakefile +0 -27
  27. data/assets/javascript/.gitkeep +0 -0
  28. data/assets/javascript/chunk.524.c3042190e39d148beaf9.js +0 -22
  29. data/assets/javascript/chunk.582.695dcbfb11a784b110cd.js +0 -22
  30. data/assets/javascript/chunk.747.a730c497582bb6742a6e.js +0 -579
  31. data/assets/javascript/chunk.761.64949d89942c829e5c1b.js +0 -1213
  32. data/assets/javascript/client-app.js +0 -1390
  33. data/assets/stylesheets/.gitkeep +0 -0
  34. data/assets/stylesheets/client-app.css +0 -1
  35. data/assets/stylesheets/vendor.css +0 -1
  36. data/build_client_app.sh +0 -10
  37. data/client-app/.editorconfig +0 -19
  38. data/client-app/.ember-cli +0 -9
  39. data/client-app/.eslintignore +0 -20
  40. data/client-app/.eslintrc +0 -10
  41. data/client-app/.gitignore +0 -23
  42. data/client-app/.prettierignore +0 -21
  43. data/client-app/.prettierrc +0 -1
  44. data/client-app/.template-lintrc.js +0 -10
  45. data/client-app/.watchmanconfig +0 -3
  46. data/client-app/README.md +0 -56
  47. data/client-app/app/app.js +0 -12
  48. data/client-app/app/components/actions-menu.js +0 -50
  49. data/client-app/app/components/back-to-site-link.js +0 -9
  50. data/client-app/app/components/back-trace.js +0 -153
  51. data/client-app/app/components/env-tab.js +0 -68
  52. data/client-app/app/components/message-info.js +0 -151
  53. data/client-app/app/components/message-row.js +0 -46
  54. data/client-app/app/components/page-nav.js +0 -42
  55. data/client-app/app/components/panel-resizer.js +0 -108
  56. data/client-app/app/components/patterns-list.js +0 -142
  57. data/client-app/app/components/tab-contents.js +0 -23
  58. data/client-app/app/components/tabbed-section.js +0 -45
  59. data/client-app/app/components/time-formatter.js +0 -36
  60. data/client-app/app/controllers/index.js +0 -269
  61. data/client-app/app/controllers/show.js +0 -39
  62. data/client-app/app/helpers/logster-url.js +0 -12
  63. data/client-app/app/helpers/or.js +0 -7
  64. data/client-app/app/index.html +0 -31
  65. data/client-app/app/initializers/app-init.js +0 -71
  66. data/client-app/app/lib/decorators.js +0 -13
  67. data/client-app/app/lib/preload.js +0 -43
  68. data/client-app/app/lib/utilities.js +0 -230
  69. data/client-app/app/models/group.js +0 -34
  70. data/client-app/app/models/message-collection.js +0 -303
  71. data/client-app/app/models/message.js +0 -128
  72. data/client-app/app/models/pattern-item.js +0 -32
  73. data/client-app/app/resolver.js +0 -3
  74. data/client-app/app/router.js +0 -13
  75. data/client-app/app/routes/index.js +0 -64
  76. data/client-app/app/routes/settings.js +0 -32
  77. data/client-app/app/routes/show.js +0 -16
  78. data/client-app/app/services/events.js +0 -4
  79. data/client-app/app/styles/app.css +0 -829
  80. data/client-app/app/templates/application.hbs +0 -1
  81. data/client-app/app/templates/components/actions-menu.hbs +0 -31
  82. data/client-app/app/templates/components/back-to-site-link.hbs +0 -8
  83. data/client-app/app/templates/components/back-trace.hbs +0 -15
  84. data/client-app/app/templates/components/env-tab.hbs +0 -10
  85. data/client-app/app/templates/components/message-info.hbs +0 -86
  86. data/client-app/app/templates/components/message-row.hbs +0 -32
  87. data/client-app/app/templates/components/page-nav.hbs +0 -37
  88. data/client-app/app/templates/components/panel-resizer.hbs +0 -3
  89. data/client-app/app/templates/components/patterns-list.hbs +0 -78
  90. data/client-app/app/templates/components/tabbed-section.hbs +0 -18
  91. data/client-app/app/templates/components/time-formatter.hbs +0 -1
  92. data/client-app/app/templates/index.hbs +0 -165
  93. data/client-app/app/templates/settings.hbs +0 -47
  94. data/client-app/app/templates/show.hbs +0 -13
  95. data/client-app/config/ember-cli-update.json +0 -18
  96. data/client-app/config/environment.js +0 -51
  97. data/client-app/config/icons.js +0 -28
  98. data/client-app/config/optional-features.json +0 -6
  99. data/client-app/config/targets.js +0 -26
  100. data/client-app/ember-cli-build.js +0 -28
  101. data/client-app/package.json +0 -76
  102. data/client-app/preload-json-manager.rb +0 -35
  103. data/client-app/public/assets/images/icon_144x144.png +0 -0
  104. data/client-app/public/assets/images/icon_64x64.png +0 -0
  105. data/client-app/testem.js +0 -23
  106. data/client-app/tests/index.html +0 -41
  107. data/client-app/tests/integration/components/back-to-site-link-test.js +0 -20
  108. data/client-app/tests/integration/components/back-trace-test.js +0 -138
  109. data/client-app/tests/integration/components/env-tab-test.js +0 -220
  110. data/client-app/tests/integration/components/message-info-test.js +0 -122
  111. data/client-app/tests/integration/components/patterns-list-test.js +0 -76
  112. data/client-app/tests/test-helper.js +0 -12
  113. data/client-app/tests/unit/controllers/index-test.js +0 -77
  114. data/client-app/tests/unit/controllers/show-test.js +0 -12
  115. data/client-app/tests/unit/routes/index-test.js +0 -11
  116. data/client-app/tests/unit/routes/show-test.js +0 -11
  117. data/client-app/yarn.lock +0 -9905
  118. data/gemfiles/rails_6.1.gemfile +0 -12
  119. data/gemfiles/rails_7.0.gemfile +0 -12
  120. data/gemfiles/rails_7.1.gemfile +0 -9
  121. data/gemfiles/rails_7.2.gemfile +0 -9
  122. data/logster.gemspec +0 -43
  123. data/test/dummy/config/application.rb +0 -31
  124. data/test/dummy/config/boot.rb +0 -6
  125. data/test/dummy/config/environment.rb +0 -6
  126. data/test/examples/test_sidekiq_reporter_example.rb +0 -47
  127. data/test/fake_data/Gemfile +0 -6
  128. data/test/fake_data/generate.rb +0 -23
  129. data/test/logster/middleware/test_reporter.rb +0 -84
  130. data/test/logster/middleware/test_viewer.rb +0 -495
  131. data/test/logster/test_base_store.rb +0 -224
  132. data/test/logster/test_cache.rb +0 -39
  133. data/test/logster/test_defer_logger.rb +0 -36
  134. data/test/logster/test_group.rb +0 -92
  135. data/test/logster/test_ignore_pattern.rb +0 -42
  136. data/test/logster/test_logger.rb +0 -177
  137. data/test/logster/test_message.rb +0 -269
  138. data/test/logster/test_pattern.rb +0 -144
  139. data/test/logster/test_railtie.rb +0 -21
  140. data/test/logster/test_redis_rate_limiter.rb +0 -246
  141. data/test/logster/test_redis_store.rb +0 -1165
  142. data/test/test_helper.rb +0 -44
@@ -0,0 +1,18 @@
1
+ {
2
+ "modulePrefix": "client-app",
3
+ "environment": "production",
4
+ "rootURL": "/logs",
5
+ "locationType": "history",
6
+ "EmberENV": {
7
+ "FEATURES": {},
8
+ "EXTEND_PROTOTYPES": {
9
+ "Date": false
10
+ },
11
+ "_APPLICATION_TEMPLATE_WRAPPER": false,
12
+ "_DEFAULT_ASYNC_OBSERVERS": true,
13
+ "_JQUERY_INTEGRATION": false,
14
+ "_TEMPLATE_ONLY_GLIMMER_COMPONENTS": true,
15
+ "_USE_EMBER_MODULES": true
16
+ },
17
+ "APP": {}
18
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "index.html": {
3
+ "file": "assets/main-B82IoCiW.js",
4
+ "name": "main",
5
+ "src": "index.html",
6
+ "isEntry": true,
7
+ "dynamicImports": [
8
+ "node_modules/.pnpm/@embroider+legacy-inspector-support@0.1.3/node_modules/@embroider/legacy-inspector-support/src/modules-4-12.js"
9
+ ],
10
+ "css": [
11
+ "assets/main-CgAFFyi8.css"
12
+ ]
13
+ },
14
+ "node_modules/.pnpm/@embroider+legacy-inspector-support@0.1.3/node_modules/@embroider/legacy-inspector-support/src/modules-4-12.js": {
15
+ "file": "assets/modules-4-12-BHA4lR2U.js",
16
+ "name": "modules-4-12",
17
+ "src": "node_modules/.pnpm/@embroider+legacy-inspector-support@0.1.3/node_modules/@embroider/legacy-inspector-support/src/modules-4-12.js",
18
+ "isDynamicEntry": true,
19
+ "imports": [
20
+ "index.html"
21
+ ]
22
+ }
23
+ }
@@ -0,0 +1 @@
1
+ :root,:host{--fa-font-solid:normal 900 1em/1 "Font Awesome 7 Free";--fa-font-regular:normal 400 1em/1 "Font Awesome 7 Free";--fa-font-light:normal 300 1em/1 "Font Awesome 7 Pro";--fa-font-thin:normal 100 1em/1 "Font Awesome 7 Pro";--fa-font-duotone:normal 900 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-regular:normal 400 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-light:normal 300 1em/1 "Font Awesome 7 Duotone";--fa-font-duotone-thin:normal 100 1em/1 "Font Awesome 7 Duotone";--fa-font-brands:normal 400 1em/1 "Font Awesome 7 Brands";--fa-font-sharp-solid:normal 900 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-regular:normal 400 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-light:normal 300 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-thin:normal 100 1em/1 "Font Awesome 7 Sharp";--fa-font-sharp-duotone-solid:normal 900 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-regular:normal 400 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-light:normal 300 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-sharp-duotone-thin:normal 100 1em/1 "Font Awesome 7 Sharp Duotone";--fa-font-slab-regular:normal 400 1em/1 "Font Awesome 7 Slab";--fa-font-slab-press-regular:normal 400 1em/1 "Font Awesome 7 Slab Press";--fa-font-slab-duo-regular:normal 400 1em/1 "Font Awesome 7 Slab Duo";--fa-font-slab-press-duo-regular:normal 400 1em/1 "Font Awesome 7 Slab Press Duo";--fa-font-pixel-regular:normal 400 1em/1 "Font Awesome 7 Pixel";--fa-font-mosaic-solid:normal 900 1em/1 "Font Awesome 7 Mosaic";--fa-font-vellum-solid:normal 900 1em/1 "Font Awesome 7 Vellum";--fa-font-whiteboard-semibold:normal 600 1em/1 "Font Awesome 7 Whiteboard";--fa-font-thumbprint-light:normal 300 1em/1 "Font Awesome 7 Thumbprint";--fa-font-notdog-solid:normal 900 1em/1 "Font Awesome 7 Notdog";--fa-font-notdog-duo-solid:normal 900 1em/1 "Font Awesome 7 Notdog Duo";--fa-font-etch-solid:normal 900 1em/1 "Font Awesome 7 Etch";--fa-font-graphite-thin:normal 100 1em/1 "Font Awesome 7 Graphite";--fa-font-jelly-regular:normal 400 1em/1 "Font Awesome 7 Jelly";--fa-font-jelly-fill-regular:normal 400 1em/1 "Font Awesome 7 Jelly Fill";--fa-font-jelly-duo-regular:normal 400 1em/1 "Font Awesome 7 Jelly Duo";--fa-font-chisel-regular:normal 400 1em/1 "Font Awesome 7 Chisel";--fa-font-utility-semibold:normal 600 1em/1 "Font Awesome 7 Utility";--fa-font-utility-duo-semibold:normal 600 1em/1 "Font Awesome 7 Utility Duo";--fa-font-utility-fill-semibold:normal 600 1em/1 "Font Awesome 7 Utility Fill"}.svg-inline--fa{box-sizing:content-box;display:var(--fa-display,inline-block);vertical-align:-.125em;height:1em;width:var(--fa-width,1.25em);overflow:visible}.svg-inline--fa.fa-2xs{vertical-align:.1em}.svg-inline--fa.fa-xs{vertical-align:0}.svg-inline--fa.fa-sm{vertical-align:-.0714286em}.svg-inline--fa.fa-lg{vertical-align:-.2em}.svg-inline--fa.fa-xl{vertical-align:-.25em}.svg-inline--fa.fa-2xl{vertical-align:-.3125em}.svg-inline--fa.fa-pull-left,.svg-inline--fa .fa-pull-start{float:inline-start;margin-inline-end:var(--fa-pull-margin,.3em)}.svg-inline--fa.fa-pull-right,.svg-inline--fa .fa-pull-end{float:inline-end;margin-inline-start:var(--fa-pull-margin,.3em)}.svg-inline--fa.fa-li{width:var(--fa-li-width,2em);inset-block-start:.25em;inset-inline-start:calc(-1 * var(--fa-li-width,2em))}.fa-layers-counter,.fa-layers-text{text-align:center;display:inline-block;position:absolute}.fa-layers{text-align:center;vertical-align:-.125em;height:1em;width:var(--fa-width,1.25em);display:inline-block;position:relative}.fa-layers .svg-inline--fa{transform-origin:50%;margin:auto;position:absolute;inset:0}.fa-layers-text{transform-origin:50%;top:50%;left:50%;transform:translate(-50%,-50%)}.fa-layers-counter{background-color:var(--fa-counter-background-color,#ff253a);border-radius:var(--fa-counter-border-radius,1em);box-sizing:border-box;color:var(--fa-inverse,#fff);line-height:var(--fa-counter-line-height,1);max-width:var(--fa-counter-max-width,5em);min-width:var(--fa-counter-min-width,1.5em);padding:var(--fa-counter-padding,.25em .5em);right:var(--fa-right,0);text-overflow:ellipsis;top:var(--fa-top,0);transform:scale(var(--fa-counter-scale,.25));transform-origin:100% 0;overflow:hidden}.fa-layers-bottom-right{bottom:var(--fa-bottom,0);right:var(--fa-right,0);transform:scale(var(--fa-layers-scale,.25));transform-origin:100% 100%;top:auto}.fa-layers-bottom-left{bottom:var(--fa-bottom,0);left:var(--fa-left,0);transform:scale(var(--fa-layers-scale,.25));transform-origin:0 100%;top:auto;right:auto}.fa-layers-top-right{top:var(--fa-top,0);right:var(--fa-right,0);transform:scale(var(--fa-layers-scale,.25));transform-origin:100% 0}.fa-layers-top-left{left:var(--fa-left,0);right:auto;top:var(--fa-top,0);transform:scale(var(--fa-layers-scale,.25));transform-origin:0 0}.fa-1x{font-size:1em}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-6x{font-size:6em}.fa-7x{font-size:7em}.fa-8x{font-size:8em}.fa-9x{font-size:9em}.fa-10x{font-size:10em}.fa-2xs{vertical-align:.225em;font-size:.625em;line-height:.1em}.fa-xs{vertical-align:.125em;font-size:.75em;line-height:.0833333em}.fa-sm{vertical-align:.0535715em;font-size:.875em;line-height:.0714286em}.fa-lg{vertical-align:-.075em;font-size:1.25em;line-height:.05em}.fa-xl{vertical-align:-.125em;font-size:1.5em;line-height:.0416667em}.fa-2xl{vertical-align:-.1875em;font-size:2em;line-height:.03125em}.fa-width-auto{--fa-width:auto}.fa-fw,.fa-width-fixed{--fa-width:1.25em}.fa-canvas-square{margin-block-end:-.125em;padding-block:.125em}.fa-canvas-roomy{box-sizing:content-box;margin-block-end:-.25em;padding-block:.25em;padding-inline:.125em}.fa-ul{margin-inline-start:var(--fa-li-margin,2.5em);padding-inline-start:0;list-style-type:none}.fa-ul>li{position:relative}.fa-li{text-align:center;width:var(--fa-li-width,2em);line-height:inherit;position:absolute;inset-inline-start:calc(-1 * var(--fa-li-width,2em))}.fa-border{border-color:var(--fa-border-color,#eee);border-radius:var(--fa-border-radius,.1em);border-style:var(--fa-border-style,solid);border-width:var(--fa-border-width,.0625em);box-sizing:var(--fa-border-box-sizing,content-box);padding:var(--fa-border-padding,.1875em .25em)}.fa-pull-left,.fa-pull-start{float:inline-start;margin-inline-end:var(--fa-pull-margin,.3em)}.fa-pull-right,.fa-pull-end{float:inline-end;margin-inline-start:var(--fa-pull-margin,.3em)}.fa-beat{animation-name:fa-beat;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-bounce{animation-name:fa-bounce;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,cubic-bezier(.28, .84, .42, 1))}.fa-fade{animation-name:fa-fade;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-beat-fade{animation-name:fa-beat-fade;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-flip{animation-name:fa-flip;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1.5s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-flip-360{animation-name:fa-flip-360;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-shake{animation-name:fa-shake;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,.75s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out)}.fa-spin{animation-name:fa-spin;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,2s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-reverse{--fa-animation-direction:reverse}.fa-pulse,.fa-spin-pulse{animation-name:fa-spin;animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,steps(8))}.fa-spin-snap{animation-name:fa-spin-snap;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,3s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-snap-4{animation-name:fa-spin-snap-4;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,2.4s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-spin-snap-8{animation-name:fa-spin-snap-8;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,4s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-buzz{animation-name:fa-buzz;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,.6s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,linear)}.fa-wag{animation-name:fa-wag;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,.9s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-out);transform-origin:bottom}.fa-float{animation-name:fa-float;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,3s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-in-out);will-change:transform}.fa-swing{animation-name:fa-swing;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,1.2s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-out);transform-origin:top}.fa-jello{animation-name:fa-jello;animation-delay:var(--fa-animation-delay,0s);animation-direction:var(--fa-animation-direction,normal);animation-duration:var(--fa-animation-duration,.9s);animation-iteration-count:var(--fa-animation-iteration-count,infinite);animation-timing-function:var(--fa-animation-timing,ease-out)}@media (prefers-reduced-motion:reduce){.fa-beat,.fa-bounce,.fa-fade,.fa-beat-fade,.fa-flip,.fa-flip-360,.fa-pulse,.fa-shake,.fa-spin,.fa-spin-pulse,.fa-buzz,.fa-float,.fa-jello,.fa-spin-snap,.fa-spin-snap-4,.fa-spin-snap-8,.fa-swing,.fa-wag{transition:none!important;animation:none!important}}@keyframes fa-beat{0%{transform:scale(1)}25%{transform:scale(calc(1.25 * var(--fa-beat-scale,1.25)))}45%{transform:scale(calc(1.22 * var(--fa-beat-scale,1.22)))}65%{transform:scale(calc(1.25 * var(--fa-beat-scale,1.25)))}90%{transform:scale(1)}}@keyframes fa-bounce{0%{animation-timing-function:var(--fa-animation-timing);transform:scale(1)translateY(0)}14%{transform:scale(var(--fa-bounce-start-scale-x,1.06), var(--fa-bounce-start-scale-y,.94)) translateY(var(--fa-bounce-anticipation,3px));animation-timing-function:cubic-bezier(.33,0,.66,.33)}32%{transform:scale(var(--fa-bounce-jump-scale-x,.94), var(--fa-bounce-jump-scale-y,1.12)) translateY(calc(-1 * var(--fa-bounce-height,.5em)));animation-timing-function:cubic-bezier(.33,.66,.66,1)}52%{transform:scale(1, 1) translateY(calc(-1 * var(--fa-bounce-height,.5em) * 1.1));animation-timing-function:cubic-bezier(.5,0,1,.5)}70%{transform:scale(var(--fa-bounce-land-scale-x,1.06), var(--fa-bounce-land-scale-y,.92)) translateY(0);animation-timing-function:cubic-bezier(.33,.33,.66,1)}85%{transform:scale(.98, 1.04) translateY(calc(-2px * var(--fa-bounce-rebound,1)));animation-timing-function:cubic-bezier(.33,0,.66,1)}to{transform:scale(1)translateY(0)}}@keyframes fa-fade{0%{opacity:1;animation-timing-function:cubic-bezier(.2,0,.4,1);transform:scale(1)}40%{opacity:var(--fa-fade-opacity,.4);animation-timing-function:cubic-bezier(.4,0,.6,1);transform:scale(.98)}to{opacity:1;transform:scale(1)}}@keyframes fa-beat-fade{0%{opacity:var(--fa-beat-fade-opacity,.4);animation-timing-function:cubic-bezier(.2,0,.4,1);transform:scale(1)}25%{opacity:calc(var(--fa-beat-fade-opacity,.4) + .4);transform:scale(var(--fa-beat-fade-scale,1.28));animation-timing-function:cubic-bezier(.4,0,.6,1)}45%{opacity:1;transform:scale(var(--fa-beat-fade-scale,1.25));animation-timing-function:cubic-bezier(.4,0,.2,1)}65%{opacity:calc(var(--fa-beat-fade-opacity,.4) + .4);transform:scale(var(--fa-beat-fade-scale,1.28));animation-timing-function:cubic-bezier(.4,0,.6,1)}to{opacity:var(--fa-beat-fade-opacity,.4);transform:scale(1)}}@keyframes fa-flip{0%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), 0deg);animation-timing-function:cubic-bezier(.2,0,.4,1)}8%{transform:perspective(2em) scale(var(--fa-flip-anticipation-scale,.95)) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), 0deg);animation-timing-function:cubic-bezier(.33,0,.66,.33)}35%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), calc(var(--fa-flip-angle,-360deg) * .6));animation-timing-function:linear}65%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), calc(var(--fa-flip-angle,-360deg) * .5));animation-timing-function:cubic-bezier(.33,.66,.66,1)}92%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), calc(var(--fa-flip-angle,-360deg) * var(--fa-flip-overshoot,1.04)));animation-timing-function:cubic-bezier(.33,0,.66,1)}to{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), var(--fa-flip-angle,-360deg))}}@keyframes fa-flip-360{0%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), 0deg);animation-timing-function:cubic-bezier(.2,0,.4,1)}8%{transform:perspective(2em) scale(var(--fa-flip-anticipation-scale,.95)) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), 0deg);animation-timing-function:cubic-bezier(.33,0,.66,.33)}50%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), calc(var(--fa-flip-angle,-360deg) * .6));animation-timing-function:cubic-bezier(.33,.66,.66,1)}80%{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), calc(var(--fa-flip-angle,-360deg) * var(--fa-flip-overshoot,1.04)));animation-timing-function:cubic-bezier(.33,0,.66,1)}to{transform:perspective(2em) scale(1) rotate3d(var(--fa-flip-x,0), var(--fa-flip-y,1), var(--fa-flip-z,0), var(--fa-flip-angle,-360deg))}}@keyframes fa-shake{0%{animation-timing-function:cubic-bezier(.2,0,.8,1);transform:rotate(0)}8%{animation-timing-function:cubic-bezier(.3,0,.7,1);transform:rotate(35deg)translate(1px)}20%{animation-timing-function:cubic-bezier(.3,0,.7,1);transform:rotate(-22deg)translate(-1px)}35%{animation-timing-function:cubic-bezier(.3,0,.7,1);transform:rotate(15deg)translate(1px)}50%{animation-timing-function:cubic-bezier(.4,0,.6,1);transform:rotate(-9deg)}65%{animation-timing-function:cubic-bezier(.4,0,.6,1);transform:rotate(5deg)}78%{animation-timing-function:cubic-bezier(.4,0,.6,1);transform:rotate(-3deg)}90%{animation-timing-function:cubic-bezier(.4,0,.2,1);transform:rotate(1deg)}to{transform:rotate(0)}}@keyframes fa-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}@keyframes fa-spin-snap{0%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(0)}12%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(60deg)}16.67%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(60deg)}28.67%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(120deg)}33.33%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(120deg)}45.33%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(180deg)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(180deg)}62%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(240deg)}66.67%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(240deg)}78.67%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(300deg)}83.33%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(300deg)}95.33%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(360deg)}to{transform:rotate(360deg)}}@keyframes fa-spin-snap-4{0%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(0)}15%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(90deg)}25%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(90deg)}40%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(180deg)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(180deg)}65%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(270deg)}75%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(270deg)}90%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(360deg)}to{transform:rotate(360deg)}}@keyframes fa-spin-snap-8{0%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(0)}9%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(45deg)}12.5%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(45deg)}21.5%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(90deg)}25%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(90deg)}34%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(135deg)}37.5%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(135deg)}46.5%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(180deg)}50%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(180deg)}59%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(225deg)}62.5%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(225deg)}71.5%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(270deg)}75%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(270deg)}84%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(315deg)}87.5%{animation-timing-function:cubic-bezier(0,0,.2,1);transform:rotate(315deg)}96.5%{animation-timing-function:cubic-bezier(.8,0,1,1);transform:rotate(360deg)}to{transform:rotate(360deg)}}@keyframes fa-buzz{0%{animation-timing-function:cubic-bezier(.1,0,.9,1);transform:translate(0)rotate(0)}5%{transform:translateX(var(--fa-buzz-distance,4px)) rotate(.5deg)}10%{transform:translateX(calc(-1 * var(--fa-buzz-distance,4px))) rotate(-.5deg)}15%{transform:translateX(var(--fa-buzz-distance,4px)) rotate(.3deg)}20%{transform:translateX(calc(-1 * var(--fa-buzz-distance,4px))) rotate(-.3deg)}25%{transform:translateX(calc(var(--fa-buzz-distance,4px) * .7)) rotate(.2deg)}30%{transform:translateX(calc(-1 * var(--fa-buzz-distance,4px) * .7)) rotate(-.2deg)}35%{transform:translateX(calc(var(--fa-buzz-distance,4px) * .4)) rotate(.1deg)}40%{transform:translate(0)rotate(0)}to{transform:translate(0)rotate(0)}}@keyframes fa-wag{0%{animation-timing-function:cubic-bezier(.2,0,.6,1);transform:rotate(0)}12%{transform:rotate(var(--fa-wag-angle,12deg));animation-timing-function:cubic-bezier(.4,0,.2,1)}24%{animation-timing-function:cubic-bezier(.2,0,.6,1);transform:rotate(2deg)}36%{transform:rotate(calc(var(--fa-wag-angle,12deg) * .85));animation-timing-function:cubic-bezier(.4,0,.2,1)}48%{animation-timing-function:cubic-bezier(.2,0,.6,1);transform:rotate(1deg)}58%{transform:rotate(calc(var(--fa-wag-angle,12deg) * .6));animation-timing-function:cubic-bezier(.4,0,.2,1)}68%{transform:rotate(0)}to{transform:rotate(0)}}@keyframes fa-float{0%{transform:translateY(0) translateX(0) rotate(0deg) scale(var(--fa-float-squash-x,1.02), var(--fa-float-squash-y,.98));animation-timing-function:cubic-bezier(.33,0,.66,.33)}15%{transform:translateY(calc(-.4 * var(--fa-float-height,6px))) translateX(var(--fa-float-drift,1px)) rotate(var(--fa-float-tilt,1deg)) scale(1, 1);animation-timing-function:cubic-bezier(.33,.66,.66,1)}35%{transform:translateY(calc(-1 * var(--fa-float-height,6px))) translateX(0) rotate(0deg) scale(var(--fa-float-stretch-x,.98), var(--fa-float-stretch-y,1.03));animation-timing-function:cubic-bezier(.5,0,.5,0)}50%{transform:translateY(calc(-.92 * var(--fa-float-height,6px))) translateX(calc(-.5 * var(--fa-float-drift,1px))) rotate(calc(-.5 * var(--fa-float-tilt,1deg))) scale(.995, 1.01);animation-timing-function:cubic-bezier(.33,0,.66,.33)}70%{transform:translateY(calc(-.3 * var(--fa-float-height,6px))) translateX(calc(-1 * var(--fa-float-drift,1px))) rotate(calc(-1 * var(--fa-float-tilt,1deg))) scale(1, 1);animation-timing-function:cubic-bezier(.33,.66,.66,1)}90%{transform:translateY(calc(.05 * var(--fa-float-height,6px))) translateX(0) rotate(0deg) scale(var(--fa-float-squash-x,1.02), var(--fa-float-squash-y,.98));animation-timing-function:cubic-bezier(.33,0,.66,1)}to{transform:translateY(0) translateX(0) rotate(0deg) scale(var(--fa-float-squash-x,1.02), var(--fa-float-squash-y,.98))}}@keyframes fa-swing{0%{animation-timing-function:cubic-bezier(.2,0,.8,1);transform:rotate(0)}8%{transform:rotate(var(--fa-swing-angle,22deg));animation-timing-function:cubic-bezier(.3,0,.7,1)}18%{transform:rotate(calc(-1 * var(--fa-swing-angle,22deg) * .85));animation-timing-function:cubic-bezier(.3,0,.7,1)}28%{transform:rotate(calc(var(--fa-swing-angle,22deg) * .65));animation-timing-function:cubic-bezier(.35,0,.65,1)}38%{transform:rotate(calc(-1 * var(--fa-swing-angle,22deg) * .45));animation-timing-function:cubic-bezier(.4,0,.6,1)}48%{transform:rotate(calc(var(--fa-swing-angle,22deg) * .25));animation-timing-function:cubic-bezier(.4,0,.6,1)}56%{transform:rotate(calc(-1 * var(--fa-swing-angle,22deg) * .1));animation-timing-function:cubic-bezier(.4,0,.6,1)}64%{transform:rotate(0)}to{transform:rotate(0)}}@keyframes fa-jello{0%{animation-timing-function:cubic-bezier(.2,0,.8,1);transform:scale(1)}12%{transform:scale(var(--fa-jello-scale-x,1.15), calc(2 - var(--fa-jello-scale-x,1.15)));animation-timing-function:cubic-bezier(.3,0,.7,1)}24%{transform:scale(calc(2 - var(--fa-jello-scale-y,1.12)), var(--fa-jello-scale-y,1.12));animation-timing-function:cubic-bezier(.3,0,.7,1)}36%{transform:scale(calc(1 + (var(--fa-jello-scale-x,1.15) - 1) * .5), calc(2 - (1 + (var(--fa-jello-scale-x,1.15) - 1) * .5)));animation-timing-function:cubic-bezier(.4,0,.6,1)}48%{transform:scale(calc(2 - (1 + (var(--fa-jello-scale-y,1.12) - 1) * .3)), calc(1 + (var(--fa-jello-scale-y,1.12) - 1) * .3));animation-timing-function:cubic-bezier(.4,0,.6,1)}58%{animation-timing-function:cubic-bezier(.4,0,.2,1);transform:scale(1.02,.98)}68%{transform:scale(1)}to{transform:scale(1)}}.fa-rotate-90{transform:rotate(90deg)}.fa-rotate-180{transform:rotate(180deg)}.fa-rotate-270{transform:rotate(270deg)}.fa-flip-horizontal{transform:scaleX(-1)}.fa-flip-vertical{transform:scaleY(-1)}.fa-flip-both,.fa-flip-horizontal.fa-flip-vertical{transform:scale(-1)}.fa-rotate-by{transform:rotate(var(--fa-rotate-angle,0))}.svg-inline--fa .fa-primary{fill:var(--fa-primary-color,currentColor);opacity:var(--fa-primary-opacity,1)}.svg-inline--fa .fa-secondary{fill:var(--fa-secondary-color,currentColor);opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-primary{opacity:var(--fa-secondary-opacity,.4)}.svg-inline--fa.fa-swap-opacity .fa-secondary{opacity:var(--fa-primary-opacity,1)}.svg-inline--fa mask .fa-primary,.svg-inline--fa mask .fa-secondary{fill:#000}.svg-inline--fa.fa-inverse{fill:var(--fa-inverse,#fff)}.fa-stack{vertical-align:middle;width:2.5em;height:2em;line-height:2em;display:inline-block;position:relative}.fa-inverse{color:var(--fa-inverse,#fff)}.svg-inline--fa.fa-stack-1x{--fa-width:1.25em;height:1em;width:var(--fa-width)}.svg-inline--fa.fa-stack-2x{--fa-width:2.5em;height:2em;width:var(--fa-width)}.fa-stack-1x,.fa-stack-2x{z-index:var(--fa-stack-z-index,auto);margin:auto;position:absolute;inset:0}body{font-family:Arial,Liberation Sans,DejaVu Sans,sans-serif;font-size:12px}body.mobile,body.mobile .message{font-size:14px}pre{font-family:Roboto Mono,Consolas,Monaco,Ubuntu Mono,monospace}table.env-table tbody tr td{vertical-align:top;border-top:none;height:18px;line-height:18px}table.env-table,table.env-table table{border-spacing:0;border-collapse:collapse}table.env-table td{padding-right:5px}tbody tr{width:98%}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading{border:4px solid #f3f3f3;border-top-color:#3498db;border-radius:50%;width:30px;height:30px;margin:40dvh auto;animation:1s linear infinite spin}.message-row-wrapper{width:inherit;display:flex}.message-row-wrapper .grouping-checkbox{margin:0}.message-row{width:inherit;font-family:Roboto;display:flex}.message-row .severity,.message-row .protected{text-align:center;flex-grow:0;flex-shrink:0;width:25px;font-size:12px}.message-row div{border-top:.5px solid #e9e9e9;padding-top:1px;padding-bottom:1px;line-height:25px}.message-row .count{box-sizing:border-box;flex-grow:0;flex-shrink:0;width:30px;padding-right:4px;font-size:11px;font-weight:700}.message-row .message-body{white-space:nowrap;text-overflow:ellipsis;flex-grow:1;flex-shrink:1;font-size:13px;overflow:hidden}.message-row .time{color:#999;vertical-align:top;flex-grow:0;flex-shrink:0;padding-right:8px;font-size:12px}.message-row .count{text-align:right}.message-row:hover{cursor:pointer;background-color:#f8f8f8}.message-row.selected{background-color:#dfdfdf}.svg-inline--fa.fatal{color:#e00}.svg-inline--fa.error{color:#900}.svg-inline--fa.warning{color:#feb800}.debug{color:#777}.action-panel .search{vertical-align:middle;box-sizing:border-box;border:1px solid #ddd;padding:3px}#log-table .show-more{text-align:center;cursor:pointer;background-color:#ddd;height:30px;margin-top:8px;line-height:30px;text-decoration:none}#bottom-panel{z-index:2;background-color:#f1f1f1;height:300px;padding:0 8px 8px;position:fixed;bottom:0;left:0;right:0}#bottom-panel.full{background-color:inherit;height:90%;position:static}#bottom-panel.full>div{padding-bottom:40px}#bottom-panel.full .tabs{display:none}#bottom-panel.full .message-info{position:static}#bottom-panel.full .message-info .content{display:block;position:static}#bottom-panel.full .save,#bottom-panel.full .share{bottom:10px}#bottom-panel.full button.delete{display:none}#bottom-panel.full .message-actions button{margin-top:8px}@media (width<=382px){#bottom-panel.full .message-actions{height:73px}}@media (width>=383px){#bottom-panel.full .message-actions{height:40px}}#bottom-panel.full .message-actions{background-color:#eee;border-top:1px solid #dfdfdf;width:100%;padding-left:10px;position:fixed;bottom:0;left:0}.message-actions{margin-right:10px;position:absolute;bottom:5px;right:0}.message-actions button{margin-left:5px}.message-actions button.copied{color:#176b3a;background:#d5f5e3}.message-actions button.copy-failed{color:#9b1c1c;background:#fde2e2}.divider{cursor:row-resize;background-color:#fafafa;border-top:1px solid #ddd;border-bottom:1px solid #ddd;height:15px;position:fixed;bottom:310px;left:0;right:0}.divider div{background-color:#ccc;width:24px;height:1px;margin:auto;position:relative}.divider .line-1{top:5px}.divider .line-2{top:6px}.divider .line-3{top:7px}#top-panel{position:fixed;inset:0 0 320px;overflow:auto}#top-panel.with-top-menu{top:35px}#top-menu{z-index:900;width:100%;position:fixed;top:0;left:0}#back-to-site-panel{background-color:#f1f1f1;height:15px;padding:10px}.message-info{border-bottom:1px solid #ddd;position:absolute;top:0;left:0;right:0}#bottom-panel.group-view .message-info{top:43px}.nav-controls.group-nav{border-bottom:1px solid #ddd;position:absolute;left:0;right:0}.action-panel{font-weight:700;position:absolute;bottom:0;left:0;right:0}.action-panel input{margin:0}.action-panel .svg-inline--fa,input,label span{vertical-align:middle}.severity-filters label{margin-right:10px}.search-clear-all .clear{float:right;vertical-align:middle}#log-table{width:99%;margin:auto}.btn span{display:inline}.hidden{display:none}.message-info pre,.message-info .env-table{margin:5px 10px 10px;position:relative}#overlay{z-index:99999;cursor:row-resize;opacity:0;position:fixed;inset:0}.message-info .content{display:none;position:absolute;inset:0 0 35px;overflow:auto}.message-info .content.active{display:block}.tabs{border-top:1px solid #ddd;margin:0 0 5px;padding:0 0 0 10px;list-style-type:none;position:absolute;bottom:13px;left:0;right:0}.tabs li{float:left;margin:0;padding-right:5px;position:relative}.tabs a{color:#333;background-color:#e1e1e1;border:1px solid #ddd;border-top:none;border-bottom-right-radius:5px;border-bottom-left-radius:5px;padding:6px;text-decoration:none;position:relative;top:6px}.tabs a.active{background-color:#f1f1f1;border-top:1px solid #f1f1f1}.btn{text-align:center;cursor:pointer;color:#333;background-color:#ddd;border:none;margin:0;padding:5px 8px;font-size:1em;font-weight:400;line-height:0;text-decoration:none;transition:all .25s;display:inline-block}.btn:hover{color:#000;background-color:#ccc}.btn .svg-inline--fa{margin-right:7px}.btn:active{text-shadow:none}.btn.danger:hover{color:#eee;background-color:#c63c1b}.btn.ok{color:#fff;background-color:#3781dc}.btn.ok:hover{color:#fff;background-color:#286dc2}.search-clear-all .search,.search-clear-all .btn.clear{height:100%}.search-clear-all .btn.clear span{vertical-align:middle}.severity-filters label,.search-clear-all .search,.search-clear-all .btn.clear{align-self:center}.search-clear-all .footer-btns .settings{box-sizing:border-box;align-items:center;height:100%;display:inline-flex}.search-clear-all{justify-content:space-between;display:flex}.search-clear-all .search{flex-shrink:2;min-width:0}.footer-btns{gap:.7em;display:flex}@media (width>=871px){.severity-filters,.search-clear-all{height:100%}.severity-filters,.more-wrapping{display:flex}.severity-filters{float:left}.action-panel{box-sizing:border-box;height:45px;padding:10px}.message-info{bottom:45px}}@media (width<=870px){.severity-filters{padding:10px}.search-clear-all{padding:0 10px 10px}.action-panel{height:69px}.message-info{bottom:69px}}@media (width<=430px){.severity-filters{white-space:nowrap;overflow:scroll hidden}.more-wrapping:before,.more-wrapping:after{content:"";height:18px;position:absolute}.more-wrapping:before{background:linear-gradient(90deg,#f1f1f1 0%,#f1f1f100 100%);width:15px;margin-left:-10px}.more-wrapping:after{background:linear-gradient(270deg,#f1f1f1 0%,#f1f1f100 100%);width:23px;right:0}}.btn.no-text .svg-inline--fa{margin:0}.btn[disabled]{opacity:.5}.actions-menu{z-index:3;background:#fafafa;flex-direction:column;width:115px;padding:5px 5px 0;display:inline-flex;position:absolute;bottom:27px;right:45px;box-shadow:0 4px 14px #00000026}.actions-menu button{height:27px;margin:0 0 5px}.nav-controls{padding:10px}#bottom-panel:not(.full) .nav-controls.env-nav{position:-moz-sticky;position:-ms-sticky;position:-o-sticky;z-index:1;background:#f1f1f1;border-bottom:1px solid #ddd;position:sticky;top:0}.current-number{margin:0 7px}.recent-link,.expand-list{color:#00f;cursor:pointer;text-decoration:underline}.settings-page{max-width:1110px;margin-left:auto;margin-right:auto;padding:0 10px 70px;font-size:15px}.settings-header{align-items:center;margin-bottom:5px;display:flex}.settings-header .header-title{flex-grow:1}.settings-header .header-logo{width:50px;height:50px}.settings-section{padding-top:15px}.settings-section .section-title{margin-top:0}.settings-section .subsection-title{margin-bottom:10px}.settings-section .tip{color:gray;font-size:13px;font-style:italic}.settings-section .pattern-wrapper{height:33px;margin-top:10px;font-size:16px;display:flex}.settings-section .api-error{color:red;margin-top:5px;font-family:Consolas,Roboto Mono,Monaco,Ubuntu Mono,monospace}.pattern-wrapper .pattern-input{width:600px;font-family:Consolas,Roboto Mono,Monaco,Ubuntu Mono,monospace;font-size:inherit;box-sizing:border-box;vertical-align:middle;flex-grow:1;flex-shrink:1;height:100%;padding:5px 8px}.retro-checkbox .checkbox{margin:0}.retro-checkbox{margin-top:7px}.btn.new-pattern{height:100%;line-height:18px}.pattern-wrapper .shrink{text-align:center;box-sizing:border-box;vertical-align:middle;flex-grow:0;flex-shrink:0;width:40px;height:100%;margin-left:10px}.svg-inline--fa{opacity:.7}.pattern-wrapper .shrink.reset{background:unset;width:unset;margin-left:8px;padding:0}.grouping-patterns button.new-pattern{margin-top:10px}.grouping-pattern-dialog-overlay{z-index:100000;background:#00000080;justify-content:center;align-items:center;display:flex;position:fixed;inset:0}.grouping-pattern-dialog{background:#fff;border-radius:4px;min-width:600px;padding:20px;box-shadow:0 4px 16px #0000004d}.grouping-pattern-dialog h3{margin:0 0 8px;font-size:1em;font-weight:700}.grouping-pattern-dialog p{color:#555;margin:0 0 10px;font-size:.9em}.grouping-pattern-input{box-sizing:border-box;border:1px solid #ccc;border-radius:3px;width:100%;margin-bottom:14px;padding:8px;font-family:monospace;font-size:1.1em}.grouping-pattern-dialog .api-error{color:#c62828;white-space:pre-wrap;margin:0 0 14px}.grouping-pattern-dialog-buttons{justify-content:flex-end;gap:8px;display:flex}.grouping-pattern-dialog .btn{padding:12px}.backtrace-line .line-link{color:#9e9e9e;white-space:normal;margin-left:3px;text-decoration:none}@media (prefers-color-scheme:dark){body{color:#cecece;background:#181818}#bottom-panel{background:#181818}.message-row div{border-top-color:#404040}.message-row:hover{background:#262626}.message-row.selected{background:#303030}#log-table .show-more,.divider{background:#262626}.divider div{background:#707070}.divider,.tabs{border-top-color:#555}#bottom-panel:not(.full) .nav-controls.env-nav,.divider,.message-info,.nav-controls.group-nav{border-bottom-color:#555}.action-panel .search{background:#262626;border-color:#404040}.btn,.tabs a{color:#cecece}.tabs a{background:#323232;border-color:#404040}.tabs a.active{color:#f4f4f4;background:#555;border-color:#707070}.btn{background:#323232}#bottom-panel.full .message-actions{background:#181818;border-color:#555}#bottom-panel:not(.full) .nav-controls.env-nav{background:#181818}.recent-link,.expand-list{color:#44f}#back-to-site-panel{background-color:#181818}.grouping-pattern-dialog{color:#cecece;background:#262626}.grouping-pattern-dialog p{color:#aaa}.grouping-pattern-input{color:#cecece;background:#181818;border-color:#555}}
@@ -5,6 +5,7 @@ module Logster
5
5
  attr_accessor(
6
6
  :allow_grouping,
7
7
  :application_version,
8
+ :authorize_request,
8
9
  :current_context,
9
10
  :env_expandable_keys,
10
11
  :enable_custom_patterns_via_ui,
data/lib/logster/group.rb CHANGED
@@ -48,7 +48,7 @@ module Logster
48
48
  end
49
49
 
50
50
  def to_json(opts = nil)
51
- JSON.fast_generate(self.to_h, opts)
51
+ JSON.generate(self.to_h, opts)
52
52
  end
53
53
 
54
54
  def add_message(message)
@@ -109,7 +109,7 @@ module Logster
109
109
  GroupWeb =
110
110
  Struct.new(*%i[regex count timestamp messages row_id]) do
111
111
  def to_json(opts = nil)
112
- JSON.fast_generate(self.to_h.merge(severity: -1, group: true), opts)
112
+ JSON.generate(self.to_h.merge(severity: -1, group: true), opts)
113
113
  end
114
114
 
115
115
  def key
@@ -65,7 +65,7 @@ module Logster
65
65
 
66
66
  def to_json(opts = nil)
67
67
  exclude_env = Hash === opts && opts.delete(:exclude_env)
68
- JSON.fast_generate(to_h(exclude_env: exclude_env), opts)
68
+ JSON.generate(to_h(exclude_env: exclude_env), opts)
69
69
  end
70
70
 
71
71
  def self.from_json(json)
@@ -134,7 +134,7 @@ module Logster
134
134
 
135
135
  # todo - memoize?
136
136
  def grouping_key
137
- Digest::SHA1.hexdigest JSON.fast_generate grouping_hash
137
+ Digest::SHA1.hexdigest JSON.generate grouping_hash
138
138
  end
139
139
 
140
140
  # todo - memoize?
@@ -273,10 +273,10 @@ module Logster
273
273
  protected
274
274
 
275
275
  def truncate_env(env, limit)
276
- if JSON.fast_generate(env).bytesize > limit
276
+ if JSON.generate(env).bytesize > limit
277
277
  sizes = {}
278
278
  braces = "{}".bytesize
279
- env.each { |k, v| sizes[k] = JSON.fast_generate(k => v).bytesize - braces }
279
+ env.each { |k, v| sizes[k] = JSON.generate(k => v).bytesize - braces }
280
280
  sorted = env.keys.sort { |a, b| sizes[a] <=> sizes[b] }
281
281
 
282
282
  kept_keys = []
@@ -5,6 +5,12 @@ module Logster
5
5
  class Reporter
6
6
  PATH_INFO = "PATH_INFO"
7
7
  SCRIPT_NAME = "SCRIPT_NAME"
8
+ REQUEST_METHOD = "REQUEST_METHOD"
9
+ SECURITY_HEADERS = {
10
+ "cache-control" => "no-store",
11
+ "referrer-policy" => "no-referrer",
12
+ "x-content-type-options" => "nosniff",
13
+ }.freeze
8
14
 
9
15
  def initialize(app, config = {})
10
16
  @app = app
@@ -20,7 +26,11 @@ module Logster
20
26
  path = script_name + path if script_name && script_name.length > 0
21
27
 
22
28
  if path == @error_path
23
- return 403, {}, ["Access Denied"] if !Logster.config.enable_js_error_reporting
29
+ if env[REQUEST_METHOD] != "POST"
30
+ return response(405, "Method not allowed", "allow" => "POST")
31
+ end
32
+ return response(403, "CSRF validation failed") unless valid_csrf_request?(env)
33
+ return response(403, "Access Denied") if !Logster.config.enable_js_error_reporting
24
34
 
25
35
  Logster
26
36
  .config
@@ -29,12 +39,12 @@ module Logster
29
39
  if Logster.config.rate_limit_error_reporting
30
40
  req = Rack::Request.new(env)
31
41
  if Logster.store.rate_limited?(req.ip, perform: true)
32
- return 429, {}, ["Rate Limited"]
42
+ return response(429, "Rate Limited")
33
43
  end
34
44
  end
35
45
  report_js_error(env)
36
46
  end
37
- return 200, {}, ["OK"]
47
+ return response(200, "OK")
38
48
  end
39
49
 
40
50
  @app.call(env)
@@ -42,6 +52,27 @@ module Logster
42
52
  Thread.current[Logster::Logger::LOGSTER_ENV] = nil
43
53
  end
44
54
 
55
+ def response(status, body, headers = {})
56
+ [
57
+ status,
58
+ SECURITY_HEADERS.merge("content-type" => "text/plain; charset=utf-8").merge(headers),
59
+ [body],
60
+ ]
61
+ end
62
+
63
+ def valid_csrf_request?(env)
64
+ fetch_site = env["HTTP_SEC_FETCH_SITE"]
65
+ return false if fetch_site && !%w[same-origin same-site none].include?(fetch_site)
66
+
67
+ same_origin_fetch = fetch_site == "same-origin"
68
+ ajax_request = env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
69
+ return false unless same_origin_fetch || ajax_request
70
+ return true if same_origin_fetch
71
+
72
+ origin = env["HTTP_ORIGIN"]
73
+ !origin || origin == Rack::Request.new(env).base_url
74
+ end
75
+
45
76
  def report_js_error(env)
46
77
  req = Rack::Request.new(env)
47
78
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "json"
4
+ require "uri"
4
5
 
5
6
  module Logster
6
7
  module Middleware
@@ -8,6 +9,39 @@ module Logster
8
9
  PATH_INFO = "PATH_INFO".freeze
9
10
  SCRIPT_NAME = "SCRIPT_NAME".freeze
10
11
  REQUEST_METHOD = "REQUEST_METHOD".freeze
12
+ ASSET_NAME = /\A[a-zA-Z0-9._-]+\z/
13
+ LEGACY_SCRIPTS = %w[vendor.js].freeze
14
+ LOGSTER_RESPONSE = "logster.response".freeze
15
+ STATIC_RESPONSE = "logster.static_response".freeze
16
+ DYNAMIC_SECURITY_HEADERS = {
17
+ "cache-control" => "no-store",
18
+ "referrer-policy" => "no-referrer",
19
+ "x-content-type-options" => "nosniff",
20
+ "x-frame-options" => "DENY",
21
+ }.freeze
22
+ STATIC_SECURITY_HEADERS = {
23
+ "cache-control" => "public, max-age=0, must-revalidate",
24
+ "referrer-policy" => "no-referrer",
25
+ "x-content-type-options" => "nosniff",
26
+ }.freeze
27
+ MESSAGE_ROUTE = %r{\A/message/([0-9a-f]+)\z}
28
+ PROTECT_ROUTE = %r{\A/protect/([0-9a-f]+)\z}
29
+ UNPROTECT_ROUTE = %r{\A/unprotect/([0-9a-f]+)\z}
30
+ SOLVE_ROUTE = %r{\A/solve/([0-9a-f]+)\z}
31
+ CLEAR_ROUTE = %r{\A/clear\z}
32
+ PATTERNS_ROUTE = %r{\A/patterns/([a-zA-Z0-9_]+)\.json\z}
33
+ RESET_COUNT_ROUTE = %r{\A/reset-count\.json\z}
34
+ SOLVE_GROUP_ROUTE = %r{\A/solve-group\z}
35
+ MUTATING_ROUTES = [
36
+ [MESSAGE_ROUTE, %w[DELETE]],
37
+ [PROTECT_ROUTE, %w[PUT]],
38
+ [UNPROTECT_ROUTE, %w[DELETE]],
39
+ [SOLVE_ROUTE, %w[PUT]],
40
+ [CLEAR_ROUTE, %w[POST]],
41
+ [PATTERNS_ROUTE, %w[POST PUT DELETE]],
42
+ [RESET_COUNT_ROUTE, %w[PUT]],
43
+ [SOLVE_GROUP_ROUTE, %w[POST]],
44
+ ].freeze
11
45
 
12
46
  def initialize(app)
13
47
  @app = app
@@ -21,60 +55,86 @@ module Logster
21
55
  end
22
56
 
23
57
  def call(env)
58
+ env.delete(LOGSTER_RESPONSE)
59
+ env.delete(STATIC_RESPONSE)
60
+ response = dispatch(env)
61
+ return response unless env.delete(LOGSTER_RESPONSE)
62
+
63
+ status, headers, body = response
64
+ security_headers =
65
+ env.delete(STATIC_RESPONSE) ? STATIC_SECURITY_HEADERS : DYNAMIC_SECURITY_HEADERS
66
+ [status, security_headers.merge(headers), body]
67
+ end
68
+
69
+ def dispatch(env)
24
70
  path = env[PATH_INFO]
25
71
  script_name = env[SCRIPT_NAME]
26
72
 
27
73
  path = script_name + path if script_name && script_name.length > 0
28
74
 
29
75
  if resource = resolve_path(path)
30
- if resource =~
31
- /\.ico$|\.js$|\.png|\.handlebars$|\.css$|\.woff$|\.ttf$|\.woff2$|\.svg$|\.otf$|\.eot$/
76
+ env[LOGSTER_RESPONSE] = true
77
+
78
+ if (authorize_request = Logster.config.authorize_request) && !authorize_request.call(env)
79
+ return not_allowed("Not authorized")
80
+ end
81
+
82
+ return not_found if parent_directory_segment?(resource)
83
+
84
+ if allowed_methods = allowed_methods_for(resource)
85
+ if !allowed_methods.include?(env[REQUEST_METHOD])
86
+ return method_not_allowed(allowed_methods)
87
+ end
88
+ return not_allowed("CSRF validation failed") unless valid_csrf_request?(env)
89
+ end
90
+
91
+ if resource =~ %r{\A/(?:.*\.(?:ico|js|png|handlebars|css|woff|ttf|woff2|svg|otf|eot))\z}
32
92
  serve_file(env, resource)
33
- elsif resource.start_with?("/messages.json") && env[REQUEST_METHOD] == "POST"
93
+ elsif resource == "/messages.json" && env[REQUEST_METHOD] == "POST"
34
94
  serve_messages(Rack::Request.new(env))
35
- elsif resource =~ %r{/message/([0-9a-f]+)$}
95
+ elsif (route_match = MESSAGE_ROUTE.match(resource))
36
96
  return method_not_allowed("DELETE") if env[REQUEST_METHOD] != "DELETE"
37
97
 
38
- key = $1
98
+ key = route_match[1]
39
99
  message = Logster.store.get(key)
40
100
  return 404, {}, ["Message not found"] unless message
41
101
 
42
102
  Logster.store.delete(message)
43
- [301, { "Location" => "#{@logs_path}/" }, []]
44
- elsif resource =~ %r{/(un)?protect/([0-9a-f]+)$}
45
- off = $1 == "un"
46
- key = $2
103
+ [303, { "location" => "#{@logs_path}/" }, []]
104
+ elsif (route_match = PROTECT_ROUTE.match(resource) || UNPROTECT_ROUTE.match(resource))
105
+ off = resource.start_with?("/unprotect/")
106
+ key = route_match[1]
47
107
 
48
108
  message = Logster.store.get(key)
49
109
  return 404, {}, ["Message not found"] unless message
50
110
 
51
111
  if off
52
112
  if Logster.store.unprotect(key)
53
- [301, { "Location" => "#{@logs_path}/show/#{key}?protected=false" }, []]
113
+ [303, { "location" => "#{@logs_path}/show/#{key}?protected=false" }, []]
54
114
  else
55
115
  [500, {}, ["Failed"]]
56
116
  end
57
117
  else
58
118
  if Logster.store.protect(key)
59
- [301, { "Location" => "#{@logs_path}/show/#{key}?protected=true" }, []]
119
+ [303, { "location" => "#{@logs_path}/show/#{key}?protected=true" }, []]
60
120
  else
61
121
  [500, {}, ["Failed"]]
62
122
  end
63
123
  end
64
- elsif resource =~ %r{/solve/([0-9a-f]+)$}
65
- key = $1
124
+ elsif (route_match = SOLVE_ROUTE.match(resource))
125
+ key = route_match[1]
66
126
 
67
127
  message = Logster.store.get(key)
68
128
  return 404, {}, ["Message not found"] unless message
69
129
 
70
130
  Logster.store.solve(key)
71
131
 
72
- [301, { "Location" => "#{@logs_path}" }, []]
73
- elsif resource =~ %r{/clear$}
132
+ [303, { "location" => "#{@logs_path}" }, []]
133
+ elsif CLEAR_ROUTE.match?(resource)
74
134
  return method_not_allowed("POST") if env[REQUEST_METHOD] != "POST"
75
135
  Logster.store.clear
76
136
  [200, {}, ["Messages cleared"]]
77
- elsif resource =~ %r{/show/([0-9a-f]+)(\.json)?$}
137
+ elsif resource =~ %r{\A/show/([0-9a-f]+)(\.json)?\z}
78
138
  key = $1
79
139
  json = $2 == ".json"
80
140
 
@@ -87,7 +147,7 @@ module Logster
87
147
  preload = { "/show/#{key}" => message }
88
148
  js_app(preload)
89
149
  end
90
- elsif resource =~ %r{/settings(\.json)?$}
150
+ elsif resource =~ %r{\A/settings(\.json)?\z}
91
151
  json = $1 == ".json"
92
152
  if json
93
153
  ignore_count = Logster.store.get_all_ignore_count
@@ -116,7 +176,7 @@ module Logster
116
176
  else
117
177
  js_app
118
178
  end
119
- elsif resource =~ %r{/patterns/([a-zA-Z0-9_]+)\.json$}
179
+ elsif (route_match = PATTERNS_ROUTE.match(resource))
120
180
  unless Logster.config.enable_custom_patterns_via_ui
121
181
  return(
122
182
  not_allowed(
@@ -125,12 +185,12 @@ module Logster
125
185
  )
126
186
  end
127
187
 
128
- set_name = $1
188
+ set_name = route_match[1]
129
189
  req = Rack::Request.new(env)
130
190
  return method_not_allowed(%w[POST PUT DELETE]) if req.request_method == "GET"
131
191
 
132
192
  update_patterns(set_name, req)
133
- elsif resource == "/reset-count.json"
193
+ elsif RESET_COUNT_ROUTE.match?(resource)
134
194
  req = Rack::Request.new(env)
135
195
  return method_not_allowed("PUT") if req.request_method != "PUT"
136
196
  pattern = nil
@@ -152,7 +212,7 @@ module Logster
152
212
  [200, {}, ["OK"]]
153
213
  elsif resource == "/"
154
214
  js_app
155
- elsif resource =~ %r{/fetch-env/([0-9a-f]+)\.json$}
215
+ elsif resource =~ %r{\A/fetch-env/([0-9a-f]+)\.json\z}
156
216
  key = $1
157
217
  env = Logster.store.get_env(key)
158
218
  if env
@@ -160,7 +220,7 @@ module Logster
160
220
  else
161
221
  not_found
162
222
  end
163
- elsif resource == "/solve-group"
223
+ elsif SOLVE_GROUP_ROUTE.match?(resource)
164
224
  return not_allowed unless Logster.config.enable_custom_patterns_via_ui
165
225
  req = Rack::Request.new(env)
166
226
  return method_not_allowed("POST") if req.request_method != "POST"
@@ -185,7 +245,30 @@ module Logster
185
245
 
186
246
  protected
187
247
 
248
+ def parent_directory_segment?(resource)
249
+ Rack::Utils.unescape_path(resource).split("/").include?("..")
250
+ rescue ArgumentError
251
+ true
252
+ end
253
+
254
+ def allowed_methods_for(resource)
255
+ route = MUTATING_ROUTES.find { |pattern, _| pattern.match?(resource) }
256
+ route&.last
257
+ end
258
+
259
+ def valid_csrf_request?(env)
260
+ return false unless env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
261
+
262
+ fetch_site = env["HTTP_SEC_FETCH_SITE"]
263
+ return false if fetch_site && !%w[same-origin same-site none].include?(fetch_site)
264
+ return true if fetch_site == "same-origin"
265
+
266
+ origin = env["HTTP_ORIGIN"]
267
+ !origin || origin == Rack::Request.new(env).base_url
268
+ end
269
+
188
270
  def serve_file(env, path)
271
+ env[STATIC_RESPONSE] = true
189
272
  env[PATH_INFO] = path
190
273
  # accl redirect is going to be trouble, ensure its bypassed
191
274
  env["sendfile.type"] = ""
@@ -307,8 +390,95 @@ module Logster
307
390
  "<script src='#{@logs_path}/javascript/#{name}' nonce='#{csp_nonce}'></script>"
308
391
  end
309
392
 
393
+ def module_script(name, csp_nonce)
394
+ "<script type='module' src='#{@logs_path}/javascript/#{name}' nonce='#{csp_nonce}'></script>"
395
+ end
396
+
397
+ def modulepreload(name, csp_nonce)
398
+ "<link rel='modulepreload' href='#{@logs_path}/javascript/#{name}' nonce='#{csp_nonce}'>"
399
+ end
400
+
401
+ # Vite's own build manifest, plus the application config the build emits
402
+ # alongside it.
403
+ def asset_manifest
404
+ @asset_manifest ||=
405
+ begin
406
+ vite = JSON.parse(read_asset("manifest.json"))
407
+ entry = vite.values.find { |chunk| chunk.is_a?(Hash) && chunk["isEntry"] }
408
+ raise KeyError, "the Vite build emitted no entry chunk" unless entry
409
+
410
+ validate_asset_manifest(
411
+ "scripts" => plain_scripts,
412
+ "module" => asset_basename(entry.fetch("file")),
413
+ "modulepreload" => preloaded_chunks(vite, entry),
414
+ "stylesheets" => Array(entry["css"]).map { |href| asset_basename(href) },
415
+ "config" => JSON.parse(read_asset("logster-config.json")),
416
+ )
417
+ rescue JSON::ParserError, KeyError, TypeError, Errno::ENOENT => error
418
+ raise RuntimeError, "Logster asset manifest is invalid: #{error.message}"
419
+ end
420
+ end
421
+
422
+ def read_asset(name)
423
+ path = File.join(@assets_path, name)
424
+ unless File.file?(path)
425
+ raise RuntimeError,
426
+ "Logster asset manifest is missing; rebuild the gem with ./build_client_app.sh"
427
+ end
428
+ File.read(path)
429
+ end
430
+
431
+ def asset_basename(path)
432
+ File.basename(path.to_s)
433
+ end
434
+
435
+ # classicEmberSupport emits this to set EmberENV before the deferred
436
+ # module entry runs. It is absent once the app no longer needs the shim.
437
+ def plain_scripts
438
+ LEGACY_SCRIPTS.select { |name| File.file?(File.join(@assets_path, "javascript", name)) }
439
+ end
440
+
441
+ # Every chunk the entry pulls in eagerly, once each, in discovery order.
442
+ def preloaded_chunks(vite, chunk, names = [], seen = {})
443
+ Array(chunk["imports"]).each do |key|
444
+ next if seen[key]
445
+ seen[key] = true
446
+
447
+ imported = vite[key]
448
+ raise KeyError, "the Vite manifest is missing chunk #{key}" unless imported
449
+
450
+ names << asset_basename(imported.fetch("file"))
451
+ preloaded_chunks(vite, imported, names, seen)
452
+ end
453
+ names
454
+ end
455
+
456
+ def validate_asset_manifest(manifest)
457
+ %w[modulepreload stylesheets].each do |type|
458
+ names = manifest.fetch(type)
459
+ unless names.is_a?(Array) && names.all? { |name| ASSET_NAME.match?(name.to_s) }
460
+ raise JSON::ParserError, "Invalid #{type} asset manifest"
461
+ end
462
+ end
463
+ unless ASSET_NAME.match?(manifest.fetch("module").to_s)
464
+ raise JSON::ParserError, "Invalid module asset manifest"
465
+ end
466
+ unless manifest["config"].is_a?(Hash)
467
+ raise JSON::ParserError, "Invalid client application config"
468
+ end
469
+
470
+ manifest
471
+ end
472
+
473
+ def encoded_client_app_config
474
+ config = JSON.parse(JSON.generate(asset_manifest.fetch("config")))
475
+ config["environment"] = "production"
476
+ config["rootURL"] = "#{@logs_path}/"
477
+ Rack::Utils.escape_html(URI.encode_uri_component(JSON.generate(config)))
478
+ end
479
+
310
480
  def to_json_and_escape(payload)
311
- Rack::Utils.escape_html(JSON.fast_generate(payload))
481
+ Rack::Utils.escape_html(JSON.generate(payload))
312
482
  end
313
483
 
314
484
  def preload_backtrace_data
@@ -344,8 +514,14 @@ module Logster
344
514
  def js_app(preload = {})
345
515
  csp_nonce = SecureRandom.hex
346
516
  preload = preloaded_data.merge(preload)
347
- root_url = @logs_path
348
- root_url += "/" if root_url[-1] != "/"
517
+ manifest = asset_manifest
518
+ stylesheets = manifest.fetch("stylesheets").map { |name| css(name, csp_nonce) }.join("\n")
519
+ javascript = [
520
+ # The plain scripts set up EmberENV, so they run before the module entry.
521
+ *manifest.fetch("scripts").map { |name| script(name, csp_nonce) },
522
+ *manifest.fetch("modulepreload").map { |name| modulepreload(name, csp_nonce) },
523
+ module_script(manifest.fetch("module"), csp_nonce),
524
+ ].join("\n")
349
525
  body = <<~HTML
350
526
  <!doctype html>
351
527
  <html>
@@ -353,18 +529,14 @@ module Logster
353
529
  <link rel="shortcut icon" href="#{@logs_path}/images/icon_64x64.png">
354
530
  <link rel="apple-touch-icon" href="#{@logs_path}/images/icon_144x144.png" />
355
531
  <title>#{Logster.config.web_title || "Logs"}</title>
356
- <link href='//fonts.googleapis.com/css?family=Roboto' rel='stylesheet' type='text/css' nonce='#{csp_nonce}'>
357
- <link href='//fonts.googleapis.com/css?family=Roboto+Mono' rel='stylesheet' type='text/css' nonce='#{csp_nonce}'>
358
532
  <meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=yes">
359
533
  <meta name="color-scheme" content="dark light">
360
- #{css("vendor.css", csp_nonce)}
361
- #{css("client-app.css", csp_nonce)}
362
- #{script("vendor.js", csp_nonce)}
534
+ #{stylesheets}
363
535
  <meta id="preloaded-data" data-root-path="#{@logs_path}" data-preloaded="#{to_json_and_escape(preload)}">
364
- <meta name="client-app/config/environment" content="%7B%22modulePrefix%22%3A%22client-app%22%2C%22environment%22%3A%22production%22%2C%22rootURL%22%3A%22#{root_url}%22%2C%22locationType%22%3A%22history%22%2C%22EmberENV%22%3A%7B%22FEATURES%22%3A%7B%7D%2C%22EXTEND_PROTOTYPES%22%3A%7B%22Date%22%3Afalse%7D%2C%22_APPLICATION_TEMPLATE_WRAPPER%22%3Afalse%2C%22_DEFAULT_ASYNC_OBSERVERS%22%3Atrue%2C%22_JQUERY_INTEGRATION%22%3Afalse%2C%22_TEMPLATE_ONLY_GLIMMER_COMPONENTS%22%3Atrue%7D%2C%22APP%22%3A%7B%22name%22%3A%22client-app%22%2C%22version%22%3A%220.0.0%2B7a424002%22%7D%2C%22exportApplicationGlobal%22%3Afalse%7D" />
536
+ <meta name="client-app/config/environment" content="#{encoded_client_app_config}" />
365
537
  </head>
366
538
  <body>
367
- #{script("client-app.js", csp_nonce)}
539
+ #{javascript}
368
540
  </body>
369
541
  </html>
370
542
  HTML
@@ -374,7 +546,7 @@ module Logster
374
546
  {
375
547
  "content-type" => "text/html; charset=utf-8",
376
548
  "content-security-policy" =>
377
- "script-src 'nonce-#{csp_nonce}'; style-src 'nonce-#{csp_nonce}'; object-src 'none'; base-uri 'none';",
549
+ "default-src 'none'; script-src 'nonce-#{csp_nonce}' 'strict-dynamic'; style-src 'self' 'nonce-#{csp_nonce}'; font-src 'self'; img-src 'self'; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; form-action 'self';",
378
550
  },
379
551
  [body],
380
552
  ]