openhab / openhab-webui

Web UIs of openHAB
Eclipse Public License 2.0
213 stars 234 forks source link

Use a custom servlet for serving files to support caching of files #1521

Closed digitaldan closed 1 year ago

digitaldan commented 1 year ago

Also, use hash in webpack builds to ensure unique names for caching.

Signed-off-by: Dan Cunningham dan@digitaldan.com

relativeci[bot] commented 1 year ago

Job #583: Bundle Size — 15.93MiB (0%).

bcc6234(current) vs c74728a main#582(baseline)

Metrics (no changes)
                 Current
Job #583
     Baseline
Job #582
Initial JS 1.71MiB 1.71MiB
Initial CSS 607.98KiB 607.98KiB
Cache Invalidation 0% 0%
Chunks 218 218
Assets 687 687
Modules 2008 2008
Duplicate Modules 110 110
Duplicate Code 1.82% 1.82%
Packages 133 133
Duplicate Packages 15 15

Total size by type (no changes)
|            |       Current
[Job #583](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M?utm_source=github&utm_medium=pr-report "View job report") |      Baseline
[Job #582](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/582-aOHRlFRe29eUP3EQJInz?utm_source=github&utm_medium=pr-report "View baseline job report") | |:--|--:|--:| | [CSS](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Atrue%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Afalse%7D%7D "View all CSS assets") | `856.02KiB` | `856.02KiB` | | [Fonts](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Atrue%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Afalse%7D%7D "View all Fonts assets") | `1.08MiB` | `1.08MiB` | | [HTML](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Atrue%2C%22ft.OTHER%22%3Afalse%7D%7D "View all HTML assets") | `1.21KiB` | `1.21KiB` | | [IMG](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Atrue%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Afalse%7D%7D "View all IMG assets") | `140.74KiB` | `140.74KiB` | | [JS](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Atrue%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Afalse%7D%7D "View all JS assets") | `9.01MiB` | `9.01MiB` | | [Media](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Atrue%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Afalse%7D%7D "View all Media assets") | `295.6KiB` | `295.6KiB` | | [Other](https://app.relative-ci.com/projects/ZNG5hy4VeSJQVQcq1Kvu/jobs/583-PTIEaRgQ1rHZnQUP6f4M/assets?ba=%7B%22filters%22%3A%7B%22ft.CSS%22%3Afalse%2C%22ft.JS%22%3Afalse%2C%22ft.IMG%22%3Afalse%2C%22ft.MEDIA%22%3Afalse%2C%22ft.FONT%22%3Afalse%2C%22ft.HTML%22%3Afalse%2C%22ft.OTHER%22%3Atrue%7D%7D "View all Other assets") | `4.58MiB` | `4.58MiB` |

View job #583 reportView main branch activity

digitaldan commented 1 year ago

Hi @ghys, i have longed noticed that the browser does not seem to do a good job caching assets from our UI and static files. This is somewhat noticeable on on a local LAN, more so if the hardware is a little slow, and on an iffy cellular connection this can be quite noticeable.

This replaces the simple file serving we did have, which did not have a concept of caching, with a servlet who will either A) use the startup time of the bundle as the "last-modified" time, which ensures all bundled files get cached appropriately or B) use the last modified time of the local file in the user's 'html' directory, which is especially nice when dealing with large images.

I also added a hash to the webpack app bundles to ensure uniqueness when loading, with the above caching this may not be absolutely necessary, but seems like a good idea in any case.

ghys commented 1 year ago

@digitaldan that's great! A new servlet will help a lot for sure!

I'd like to have this PR as an opportunity to discuss further improvements and fixes:

I've found an implementation of a servlet that seems to tackle both: https://balusc.omnifaces.org/2009/02/fileservlet-supporting-resume-and.html Maybe it can be an inspiration in our servlet (notice it's LGPL-licensed).

For GZip compression the example above will perform the compression on the fly (with a java.util.zip.GZIPOutputStream) for all assets (like static images), which is great, but the suggestion in the case of main UI assets (better imo) was to let webpack pre-generate the compressed assets and have the servlet just serve them as-is if they exist, with a Content-Encoding: gzip or Content-Encoding: br response header.

I've done that just now as a test, following https://github.com/webpack-contrib/compression-webpack-plugin/tree/v6.0.3#multiple-compressed-versions-of-assets-for-different-algorithm (changed minRatio to Infinity)

$ npm install compression-webpack-plugin@6.0.3 --save-dev

(compression-webpack-plugin@6.0.3 is the last version compatible with webpack 4)

The results are as follows, the potential gains are impressive:


                                                Asset        Size  Chunks                         Chunk Names
                                         ./index.html    1.21 KiB          [emitted]
                                        css/0.app.css    69 bytes       0  [emitted]
                                     css/0.app.css.br    43 bytes          [emitted]
                                     css/0.app.css.gz    85 bytes          [emitted]
                                    css/0.app.css.map   499 bytes          [emitted]
                                       css/14.app.css    1.88 KiB      14  [emitted]              about-page
                                    css/14.app.css.br   498 bytes          [emitted]
                                    css/14.app.css.gz   641 bytes          [emitted]
                                   css/14.app.css.map     3.8 KiB          [emitted]
                                       css/15.app.css    9.02 KiB      15  [emitted]              admin-base
                                    css/15.app.css.br    1.69 KiB          [emitted]
                                    css/15.app.css.gz    1.98 KiB          [emitted]
                                   css/15.app.css.map    17.4 KiB          [emitted]
                                       css/16.app.css    5.68 KiB      16  [emitted]              admin-config
                                    css/16.app.css.br    1.27 KiB          [emitted]
                                    css/16.app.css.gz    1.52 KiB          [emitted]
                                   css/16.app.css.map    14.4 KiB          [emitted]
                                       css/17.app.css    3.51 KiB      17  [emitted]              admin-devtools
                                    css/17.app.css.br   768 bytes          [emitted]
                                    css/17.app.css.gz   950 bytes          [emitted]
                                   css/17.app.css.map    6.22 KiB          [emitted]
                                       css/18.app.css    2.98 KiB      18  [emitted]              admin-pages
                                    css/18.app.css.br   745 bytes          [emitted]
                                    css/18.app.css.gz   887 bytes          [emitted]
                                   css/18.app.css.map    7.43 KiB          [emitted]
                                       css/19.app.css       2 KiB   19, 6  [emitted]              admin-pages-echarts
                                    css/19.app.css.br   447 bytes          [emitted]
                                    css/19.app.css.gz   561 bytes          [emitted]
                                   css/19.app.css.map    4.54 KiB          [emitted]
                                       css/20.app.css    2.99 KiB   20, 7  [emitted]              admin-pages-leaflet
                                    css/20.app.css.br   479 bytes          [emitted]
                                    css/20.app.css.gz   576 bytes          [emitted]
                                   css/20.app.css.map    6.06 KiB          [emitted]
                                       css/21.app.css     1.3 KiB      21  [emitted]              admin-rules
                                    css/21.app.css.br   407 bytes          [emitted]
                                    css/21.app.css.gz   524 bytes          [emitted]
                                   css/21.app.css.map    3.56 KiB          [emitted]
                                       css/22.app.css   127 bytes      22  [emitted]              admin-schedule
                                    css/22.app.css.br    79 bytes          [emitted]
                                    css/22.app.css.gz   105 bytes          [emitted]
                                   css/22.app.css.map   624 bytes          [emitted]
                                       css/23.app.css   583 bytes      23  [emitted]              analyzer
                                    css/23.app.css.br   237 bytes          [emitted]
                                    css/23.app.css.gz   288 bytes          [emitted]
                                   css/23.app.css.map    1.44 KiB          [emitted]
                                       css/24.app.css    1.17 KiB      24  [emitted]              blockly-editor
                                    css/24.app.css.br   331 bytes          [emitted]
                                    css/24.app.css.gz   427 bytes          [emitted]
                                   css/24.app.css.map    3.03 KiB          [emitted]
                                       css/25.app.css     1.1 KiB  25, 48  [emitted]              config-parameter
                                    css/25.app.css.br   366 bytes          [emitted]
                                    css/25.app.css.gz   495 bytes          [emitted]
                                   css/25.app.css.map    2.96 KiB          [emitted]
                                       css/26.app.css   175 bytes      26  [emitted]              cronexpression-editor
                                    css/26.app.css.br    92 bytes          [emitted]
                                    css/26.app.css.gz   129 bytes          [emitted]
                                   css/26.app.css.map   653 bytes          [emitted]
                                       css/27.app.css    1.42 KiB      27  [emitted]              habot
                                    css/27.app.css.br   366 bytes          [emitted]
                                    css/27.app.css.gz   502 bytes          [emitted]
                                   css/27.app.css.map    2.89 KiB          [emitted]
                                        css/3.app.css    14.6 KiB       3  [emitted]              vendors~admin-pages-leaflet~location-picker~map-page~plan-page~script-editor
                                     css/3.app.css.br    5.46 KiB          [emitted]
                                     css/3.app.css.gz    6.13 KiB          [emitted]
                                    css/3.app.css.map    23.1 KiB          [emitted]
                                       css/30.app.css   109 bytes      30  [emitted]              oh-chart-component
                                    css/30.app.css.br    49 bytes          [emitted]
                                    css/30.app.css.gz    94 bytes          [emitted]
                                   css/30.app.css.map   591 bytes          [emitted]
                                       css/33.app.css   875 bytes      33  [emitted]              profile-page
                                    css/33.app.css.br   212 bytes          [emitted]
                                    css/33.app.css.gz   303 bytes          [emitted]
                                   css/33.app.css.map    1.77 KiB          [emitted]
                                       css/34.app.css    14.8 KiB   34, 7  [emitted]              script-editor
                                    css/34.app.css.br    3.48 KiB          [emitted]
                                    css/34.app.css.gz    3.99 KiB          [emitted]
                                   css/34.app.css.map    27.1 KiB          [emitted]
                                       css/35.app.css   299 bytes      35  [emitted]              setup-wizard
                                    css/35.app.css.br   137 bytes          [emitted]
                                    css/35.app.css.gz   183 bytes          [emitted]
                                   css/35.app.css.map   847 bytes          [emitted]
                                       css/37.app.css   765 bytes      37  [emitted]              vendors~canvas-layout
                                    css/37.app.css.br   262 bytes          [emitted]
                                    css/37.app.css.gz   317 bytes          [emitted]
                                   css/37.app.css.map    1.48 KiB          [emitted]
                                       css/40.app.css    39.2 KiB      40  [emitted]              vendors~oh-video-videojs
                                    css/40.app.css.br    8.95 KiB          [emitted]
                                    css/40.app.css.gz    10.1 KiB          [emitted]
                                   css/40.app.css.map    56.4 KiB          [emitted]
                                       css/42.app.css     140 KiB      42  [emitted]              vendors~swagger-css
                                    css/42.app.css.br    15.6 KiB          [emitted]
                                    css/42.app.css.gz    21.6 KiB          [emitted]
                                   css/42.app.css.map     197 KiB          [emitted]
                                       css/45.app.css   234 bytes      45  [emitted]              zwave-network
                                    css/45.app.css.br   117 bytes          [emitted]
                                    css/45.app.css.gz   170 bytes          [emitted]
                                   css/45.app.css.map   801 bytes          [emitted]
                                       css/48.app.css   256 bytes      48  [emitted]
                                    css/48.app.css.br   112 bytes          [emitted]
                                    css/48.app.css.gz   145 bytes          [emitted]
                                   css/48.app.css.map   758 bytes          [emitted]
                                        css/5.app.css    34 bytes       5  [emitted]              vendors~admin-pages-echarts~oh-chart-component~zwave-network
                                     css/5.app.css.br    36 bytes          [emitted]
                                     css/5.app.css.gz    51 bytes          [emitted]
                                    css/5.app.css.map   435 bytes          [emitted]
                                        css/6.app.css   753 bytes       6  [emitted]              chart-page
                                     css/6.app.css.br   171 bytes          [emitted]
                                     css/6.app.css.gz   224 bytes          [emitted]
                                    css/6.app.css.map    1.31 KiB          [emitted]
                                        css/7.app.css   360 bytes       7  [emitted]              map-page
                                     css/7.app.css.br   133 bytes          [emitted]
                                     css/7.app.css.gz   174 bytes          [emitted]
                                    css/7.app.css.map   851 bytes          [emitted]
                                        css/8.app.css    1.74 KiB       8  [emitted]              plan-page
                                     css/8.app.css.br   317 bytes          [emitted]
                                     css/8.app.css.gz   383 bytes          [emitted]
                                    css/8.app.css.map    2.73 KiB          [emitted]
                                          css/app.css     608 KiB      29  [emitted]              main
                                       css/app.css.br    66.5 KiB          [emitted]
                                       css/app.css.gz    86.9 KiB          [emitted]
                                      css/app.css.map     917 KiB          [emitted]
                    fonts/Framework7Icons-Regular.ttf     292 KiB          [emitted]
                   fonts/Framework7Icons-Regular.woff     142 KiB          [emitted]
                  fonts/Framework7Icons-Regular.woff2     105 KiB          [emitted]
                      fonts/MaterialIcons-Regular.ttf     311 KiB          [emitted]
                     fonts/MaterialIcons-Regular.woff     144 KiB          [emitted]
                    fonts/MaterialIcons-Regular.woff2     112 KiB          [emitted]
                        images/openhab-logo-white.svg    17.7 KiB          [emitted]
                     images/openhab-logo-white.svg.br    5.63 KiB          [emitted]
                              images/openhab-logo.svg    17.7 KiB          [emitted]
                           images/openhab-logo.svg.br    5.67 KiB          [emitted]
                     js/0.app.035e53999bb6cb9d1b32.js     118 KiB       0  [emitted] [immutable]
                  js/0.app.035e53999bb6cb9d1b32.js.br    30.7 KiB          [emitted] [immutable]
                  js/0.app.035e53999bb6cb9d1b32.js.gz    37.1 KiB          [emitted] [immutable]
                     js/1.app.035e53999bb6cb9d1b32.js    21.8 KiB       1  [emitted] [immutable]
                  js/1.app.035e53999bb6cb9d1b32.js.br    6.06 KiB          [emitted] [immutable]
                  js/1.app.035e53999bb6cb9d1b32.js.gz    6.75 KiB          [emitted] [immutable]
                    js/10.app.035e53999bb6cb9d1b32.js    11.6 KiB      10  [emitted] [immutable]  vendors~admin-config~admin-rules
                 js/10.app.035e53999bb6cb9d1b32.js.br    2.99 KiB          [emitted] [immutable]
                 js/10.app.035e53999bb6cb9d1b32.js.gz    3.33 KiB          [emitted] [immutable]
                   js/100.app.035e53999bb6cb9d1b32.js   968 bytes     100  [emitted] [immutable]
                js/100.app.035e53999bb6cb9d1b32.js.br   546 bytes          [emitted] [immutable]
                js/100.app.035e53999bb6cb9d1b32.js.gz   568 bytes          [emitted] [immutable]
                   js/101.app.035e53999bb6cb9d1b32.js       1 KiB     101  [emitted] [immutable]
                js/101.app.035e53999bb6cb9d1b32.js.br   567 bytes          [emitted] [immutable]
                js/101.app.035e53999bb6cb9d1b32.js.gz   605 bytes          [emitted] [immutable]
                   js/102.app.035e53999bb6cb9d1b32.js    1.02 KiB     102  [emitted] [immutable]
                js/102.app.035e53999bb6cb9d1b32.js.br   570 bytes          [emitted] [immutable]
                js/102.app.035e53999bb6cb9d1b32.js.gz   619 bytes          [emitted] [immutable]
                   js/103.app.035e53999bb6cb9d1b32.js   788 bytes     103  [emitted] [immutable]
                js/103.app.035e53999bb6cb9d1b32.js.br   411 bytes          [emitted] [immutable]
                js/103.app.035e53999bb6cb9d1b32.js.gz   467 bytes          [emitted] [immutable]
                   js/104.app.035e53999bb6cb9d1b32.js   832 bytes     104  [emitted] [immutable]
                js/104.app.035e53999bb6cb9d1b32.js.br   421 bytes          [emitted] [immutable]
                js/104.app.035e53999bb6cb9d1b32.js.gz   474 bytes          [emitted] [immutable]
                   js/105.app.035e53999bb6cb9d1b32.js    1.54 KiB     105  [emitted] [immutable]
                js/105.app.035e53999bb6cb9d1b32.js.br   604 bytes          [emitted] [immutable]
                js/105.app.035e53999bb6cb9d1b32.js.gz   682 bytes          [emitted] [immutable]
                   js/106.app.035e53999bb6cb9d1b32.js    1.34 KiB     106  [emitted] [immutable]
                js/106.app.035e53999bb6cb9d1b32.js.br   513 bytes          [emitted] [immutable]
                js/106.app.035e53999bb6cb9d1b32.js.gz   619 bytes          [emitted] [immutable]
                   js/107.app.035e53999bb6cb9d1b32.js    1.43 KiB     107  [emitted] [immutable]
                js/107.app.035e53999bb6cb9d1b32.js.br   566 bytes          [emitted] [immutable]
                js/107.app.035e53999bb6cb9d1b32.js.gz   676 bytes          [emitted] [immutable]
                   js/108.app.035e53999bb6cb9d1b32.js    1.16 KiB     108  [emitted] [immutable]
                js/108.app.035e53999bb6cb9d1b32.js.br   632 bytes          [emitted] [immutable]
                js/108.app.035e53999bb6cb9d1b32.js.gz   674 bytes          [emitted] [immutable]
                   js/109.app.035e53999bb6cb9d1b32.js   913 bytes     109  [emitted] [immutable]
                js/109.app.035e53999bb6cb9d1b32.js.br   477 bytes          [emitted] [immutable]
                js/109.app.035e53999bb6cb9d1b32.js.gz   535 bytes          [emitted] [immutable]
                    js/11.app.035e53999bb6cb9d1b32.js    16.9 KiB      11  [emitted] [immutable]  vendors~admin-rules~config-parameter
                 js/11.app.035e53999bb6cb9d1b32.js.br    4.17 KiB          [emitted] [immutable]
                 js/11.app.035e53999bb6cb9d1b32.js.gz    4.63 KiB          [emitted] [immutable]
                   js/110.app.035e53999bb6cb9d1b32.js    1.42 KiB     110  [emitted] [immutable]
                js/110.app.035e53999bb6cb9d1b32.js.br   576 bytes          [emitted] [immutable]
                js/110.app.035e53999bb6cb9d1b32.js.gz   622 bytes          [emitted] [immutable]
                   js/111.app.035e53999bb6cb9d1b32.js    1.22 KiB     111  [emitted] [immutable]
                js/111.app.035e53999bb6cb9d1b32.js.br   562 bytes          [emitted] [immutable]
                js/111.app.035e53999bb6cb9d1b32.js.gz   661 bytes          [emitted] [immutable]
                   js/112.app.035e53999bb6cb9d1b32.js   945 bytes     112  [emitted] [immutable]
                js/112.app.035e53999bb6cb9d1b32.js.br   495 bytes          [emitted] [immutable]
                js/112.app.035e53999bb6cb9d1b32.js.gz   553 bytes          [emitted] [immutable]
                   js/113.app.035e53999bb6cb9d1b32.js   803 bytes     113  [emitted] [immutable]
                js/113.app.035e53999bb6cb9d1b32.js.br   429 bytes          [emitted] [immutable]
                js/113.app.035e53999bb6cb9d1b32.js.gz   483 bytes          [emitted] [immutable]
                   js/114.app.035e53999bb6cb9d1b32.js   757 bytes     114  [emitted] [immutable]
                js/114.app.035e53999bb6cb9d1b32.js.br   407 bytes          [emitted] [immutable]
                js/114.app.035e53999bb6cb9d1b32.js.gz   454 bytes          [emitted] [immutable]
                   js/115.app.035e53999bb6cb9d1b32.js   955 bytes     115  [emitted] [immutable]
                js/115.app.035e53999bb6cb9d1b32.js.br   504 bytes          [emitted] [immutable]
                js/115.app.035e53999bb6cb9d1b32.js.gz   562 bytes          [emitted] [immutable]
                   js/116.app.035e53999bb6cb9d1b32.js    1.05 KiB     116  [emitted] [immutable]
                js/116.app.035e53999bb6cb9d1b32.js.br   488 bytes          [emitted] [immutable]
                js/116.app.035e53999bb6cb9d1b32.js.gz   556 bytes          [emitted] [immutable]
                   js/117.app.035e53999bb6cb9d1b32.js   975 bytes     117  [emitted] [immutable]
                js/117.app.035e53999bb6cb9d1b32.js.br   511 bytes          [emitted] [immutable]
                js/117.app.035e53999bb6cb9d1b32.js.gz   562 bytes          [emitted] [immutable]
                   js/118.app.035e53999bb6cb9d1b32.js    1.47 KiB     118  [emitted] [immutable]
                js/118.app.035e53999bb6cb9d1b32.js.br   570 bytes          [emitted] [immutable]
                js/118.app.035e53999bb6cb9d1b32.js.gz   654 bytes          [emitted] [immutable]
                   js/119.app.035e53999bb6cb9d1b32.js    1.17 KiB     119  [emitted] [immutable]
                js/119.app.035e53999bb6cb9d1b32.js.br   542 bytes          [emitted] [immutable]
                js/119.app.035e53999bb6cb9d1b32.js.gz   643 bytes          [emitted] [immutable]
                    js/12.app.035e53999bb6cb9d1b32.js     114 KiB      12  [emitted] [immutable]  vendors~oh-chart-component~zwave-network
                 js/12.app.035e53999bb6cb9d1b32.js.br    32.9 KiB          [emitted] [immutable]
                 js/12.app.035e53999bb6cb9d1b32.js.gz    37.4 KiB          [emitted] [immutable]
                   js/120.app.035e53999bb6cb9d1b32.js    1.38 KiB     120  [emitted] [immutable]
                js/120.app.035e53999bb6cb9d1b32.js.br   544 bytes          [emitted] [immutable]
                js/120.app.035e53999bb6cb9d1b32.js.gz   646 bytes          [emitted] [immutable]
                   js/121.app.035e53999bb6cb9d1b32.js    1.57 KiB     121  [emitted] [immutable]
                js/121.app.035e53999bb6cb9d1b32.js.br   590 bytes          [emitted] [immutable]
                js/121.app.035e53999bb6cb9d1b32.js.gz   681 bytes          [emitted] [immutable]
                   js/122.app.035e53999bb6cb9d1b32.js    1.07 KiB     122  [emitted] [immutable]
                js/122.app.035e53999bb6cb9d1b32.js.br   483 bytes          [emitted] [immutable]
                js/122.app.035e53999bb6cb9d1b32.js.gz   559 bytes          [emitted] [immutable]
                   js/123.app.035e53999bb6cb9d1b32.js    1.44 KiB     123  [emitted] [immutable]
                js/123.app.035e53999bb6cb9d1b32.js.br   578 bytes          [emitted] [immutable]
                js/123.app.035e53999bb6cb9d1b32.js.gz   648 bytes          [emitted] [immutable]
                   js/124.app.035e53999bb6cb9d1b32.js    1.17 KiB     124  [emitted] [immutable]
                js/124.app.035e53999bb6cb9d1b32.js.br   551 bytes          [emitted] [immutable]
                js/124.app.035e53999bb6cb9d1b32.js.gz   647 bytes          [emitted] [immutable]
                   js/125.app.035e53999bb6cb9d1b32.js   800 bytes     125  [emitted] [immutable]
                js/125.app.035e53999bb6cb9d1b32.js.br   423 bytes          [emitted] [immutable]
                js/125.app.035e53999bb6cb9d1b32.js.gz   476 bytes          [emitted] [immutable]
                   js/126.app.035e53999bb6cb9d1b32.js    1.41 KiB     126  [emitted] [immutable]
                js/126.app.035e53999bb6cb9d1b32.js.br   541 bytes          [emitted] [immutable]
                js/126.app.035e53999bb6cb9d1b32.js.gz   635 bytes          [emitted] [immutable]
                   js/127.app.035e53999bb6cb9d1b32.js     1.7 KiB     127  [emitted] [immutable]
                js/127.app.035e53999bb6cb9d1b32.js.br   693 bytes          [emitted] [immutable]
                js/127.app.035e53999bb6cb9d1b32.js.gz   753 bytes          [emitted] [immutable]
                   js/128.app.035e53999bb6cb9d1b32.js  1010 bytes     128  [emitted] [immutable]
                js/128.app.035e53999bb6cb9d1b32.js.br   529 bytes          [emitted] [immutable]
                js/128.app.035e53999bb6cb9d1b32.js.gz   579 bytes          [emitted] [immutable]
                   js/129.app.035e53999bb6cb9d1b32.js   756 bytes     129  [emitted] [immutable]
                js/129.app.035e53999bb6cb9d1b32.js.br   400 bytes          [emitted] [immutable]
                js/129.app.035e53999bb6cb9d1b32.js.gz   448 bytes          [emitted] [immutable]
                    js/13.app.035e53999bb6cb9d1b32.js     132 KiB      13  [emitted] [immutable]
                 js/13.app.035e53999bb6cb9d1b32.js.br    36.9 KiB          [emitted] [immutable]  
                 js/13.app.035e53999bb6cb9d1b32.js.gz    42.1 KiB          [emitted] [immutable]
                   js/130.app.035e53999bb6cb9d1b32.js       1 KiB     130  [emitted] [immutable]
                js/130.app.035e53999bb6cb9d1b32.js.br   550 bytes          [emitted] [immutable]
                js/130.app.035e53999bb6cb9d1b32.js.gz   597 bytes          [emitted] [immutable]
                   js/131.app.035e53999bb6cb9d1b32.js    1.15 KiB     131  [emitted] [immutable]
                js/131.app.035e53999bb6cb9d1b32.js.br   548 bytes          [emitted] [immutable]
                js/131.app.035e53999bb6cb9d1b32.js.gz   649 bytes          [emitted] [immutable]
                   js/132.app.035e53999bb6cb9d1b32.js    1.66 KiB     132  [emitted] [immutable]
                js/132.app.035e53999bb6cb9d1b32.js.br   635 bytes          [emitted] [immutable]
                js/132.app.035e53999bb6cb9d1b32.js.gz   734 bytes          [emitted] [immutable]
                   js/133.app.035e53999bb6cb9d1b32.js    1.26 KiB     133  [emitted] [immutable]
                js/133.app.035e53999bb6cb9d1b32.js.br   554 bytes          [emitted] [immutable]
                js/133.app.035e53999bb6cb9d1b32.js.gz   642 bytes          [emitted] [immutable]
                   js/134.app.035e53999bb6cb9d1b32.js    1.22 KiB     134  [emitted] [immutable]
                js/134.app.035e53999bb6cb9d1b32.js.br   499 bytes          [emitted] [immutable]
                js/134.app.035e53999bb6cb9d1b32.js.gz   568 bytes          [emitted] [immutable]
                   js/135.app.035e53999bb6cb9d1b32.js   939 bytes     135  [emitted] [immutable]
                js/135.app.035e53999bb6cb9d1b32.js.br   490 bytes          [emitted] [immutable]
                js/135.app.035e53999bb6cb9d1b32.js.gz   549 bytes          [emitted] [immutable]
                   js/136.app.035e53999bb6cb9d1b32.js   924 bytes     136  [emitted] [immutable]
                js/136.app.035e53999bb6cb9d1b32.js.br   478 bytes          [emitted] [immutable]
                js/136.app.035e53999bb6cb9d1b32.js.gz   538 bytes          [emitted] [immutable]
                   js/137.app.035e53999bb6cb9d1b32.js   954 bytes     137  [emitted] [immutable]
                js/137.app.035e53999bb6cb9d1b32.js.br   545 bytes          [emitted] [immutable]
                js/137.app.035e53999bb6cb9d1b32.js.gz   594 bytes          [emitted] [immutable]
                   js/138.app.035e53999bb6cb9d1b32.js    1.47 KiB     138  [emitted] [immutable]
                js/138.app.035e53999bb6cb9d1b32.js.br   580 bytes          [emitted] [immutable]
                js/138.app.035e53999bb6cb9d1b32.js.gz   690 bytes          [emitted] [immutable]
                   js/139.app.035e53999bb6cb9d1b32.js   971 bytes     139  [emitted] [immutable]
                js/139.app.035e53999bb6cb9d1b32.js.br   498 bytes          [emitted] [immutable]
                js/139.app.035e53999bb6cb9d1b32.js.gz   551 bytes          [emitted] [immutable]
                    js/14.app.035e53999bb6cb9d1b32.js    98.1 KiB      14  [emitted] [immutable]  about-page
                 js/14.app.035e53999bb6cb9d1b32.js.br    20.8 KiB          [emitted] [immutable]
                 js/14.app.035e53999bb6cb9d1b32.js.gz    25.4 KiB          [emitted] [immutable]
                   js/140.app.035e53999bb6cb9d1b32.js     1.6 KiB     140  [emitted] [immutable]
                js/140.app.035e53999bb6cb9d1b32.js.br   642 bytes          [emitted] [immutable]
                js/140.app.035e53999bb6cb9d1b32.js.gz   736 bytes          [emitted] [immutable]
                   js/141.app.035e53999bb6cb9d1b32.js   978 bytes     141  [emitted] [immutable]
                js/141.app.035e53999bb6cb9d1b32.js.br   520 bytes          [emitted] [immutable]
                js/141.app.035e53999bb6cb9d1b32.js.gz   567 bytes          [emitted] [immutable]
                   js/142.app.035e53999bb6cb9d1b32.js   970 bytes     142  [emitted] [immutable]
                js/142.app.035e53999bb6cb9d1b32.js.br   498 bytes          [emitted] [immutable]
                js/142.app.035e53999bb6cb9d1b32.js.gz   554 bytes          [emitted] [immutable]
                   js/143.app.035e53999bb6cb9d1b32.js   961 bytes     143  [emitted] [immutable]
                js/143.app.035e53999bb6cb9d1b32.js.br   493 bytes          [emitted] [immutable]
                js/143.app.035e53999bb6cb9d1b32.js.gz   548 bytes          [emitted] [immutable]
                   js/144.app.035e53999bb6cb9d1b32.js   965 bytes     144  [emitted] [immutable]
                js/144.app.035e53999bb6cb9d1b32.js.br   511 bytes          [emitted] [immutable]
                js/144.app.035e53999bb6cb9d1b32.js.gz   568 bytes          [emitted] [immutable]
                   js/145.app.035e53999bb6cb9d1b32.js     1.5 KiB     145  [emitted] [immutable]
                js/145.app.035e53999bb6cb9d1b32.js.br   551 bytes          [emitted] [immutable]
                js/145.app.035e53999bb6cb9d1b32.js.gz   640 bytes          [emitted] [immutable]
                   js/146.app.035e53999bb6cb9d1b32.js    1.42 KiB     146  [emitted] [immutable]
                js/146.app.035e53999bb6cb9d1b32.js.br   730 bytes          [emitted] [immutable]
                js/146.app.035e53999bb6cb9d1b32.js.gz   798 bytes          [emitted] [immutable]
                   js/147.app.035e53999bb6cb9d1b32.js  1010 bytes     147  [emitted] [immutable]
                js/147.app.035e53999bb6cb9d1b32.js.br   519 bytes          [emitted] [immutable]
                js/147.app.035e53999bb6cb9d1b32.js.gz   580 bytes          [emitted] [immutable]
                   js/148.app.035e53999bb6cb9d1b32.js  1020 bytes     148  [emitted] [immutable]
                js/148.app.035e53999bb6cb9d1b32.js.br   524 bytes          [emitted] [immutable]
                js/148.app.035e53999bb6cb9d1b32.js.gz   584 bytes          [emitted] [immutable]
                   js/149.app.035e53999bb6cb9d1b32.js   950 bytes     149  [emitted] [immutable]
                js/149.app.035e53999bb6cb9d1b32.js.br   510 bytes          [emitted] [immutable]
                js/149.app.035e53999bb6cb9d1b32.js.gz   564 bytes          [emitted] [immutable]
                    js/15.app.035e53999bb6cb9d1b32.js    61.6 KiB      15  [emitted] [immutable]  admin-base
                 js/15.app.035e53999bb6cb9d1b32.js.br    11.7 KiB          [emitted] [immutable]
                 js/15.app.035e53999bb6cb9d1b32.js.gz    14.4 KiB          [emitted] [immutable]
                   js/150.app.035e53999bb6cb9d1b32.js    1.97 KiB     150  [emitted] [immutable]
                js/150.app.035e53999bb6cb9d1b32.js.br   808 bytes          [emitted] [immutable]
                js/150.app.035e53999bb6cb9d1b32.js.gz   973 bytes          [emitted] [immutable]
                   js/151.app.035e53999bb6cb9d1b32.js   783 bytes     151  [emitted] [immutable]
                js/151.app.035e53999bb6cb9d1b32.js.br   443 bytes          [emitted] [immutable]
                js/151.app.035e53999bb6cb9d1b32.js.gz   487 bytes          [emitted] [immutable]
                   js/152.app.035e53999bb6cb9d1b32.js    1.17 KiB     152  [emitted] [immutable]
                js/152.app.035e53999bb6cb9d1b32.js.br   491 bytes          [emitted] [immutable]
                js/152.app.035e53999bb6cb9d1b32.js.gz   590 bytes          [emitted] [immutable]
                   js/153.app.035e53999bb6cb9d1b32.js    1.09 KiB     153  [emitted] [immutable]
                js/153.app.035e53999bb6cb9d1b32.js.br   598 bytes          [emitted] [immutable]
                js/153.app.035e53999bb6cb9d1b32.js.gz   626 bytes          [emitted] [immutable]
                   js/154.app.035e53999bb6cb9d1b32.js    1.47 KiB     154  [emitted] [immutable]
                js/154.app.035e53999bb6cb9d1b32.js.br   605 bytes          [emitted] [immutable]
                js/154.app.035e53999bb6cb9d1b32.js.gz   688 bytes          [emitted] [immutable]
                   js/155.app.035e53999bb6cb9d1b32.js    1.47 KiB     155  [emitted] [immutable]
                js/155.app.035e53999bb6cb9d1b32.js.br   674 bytes          [emitted] [immutable]
                js/155.app.035e53999bb6cb9d1b32.js.gz   739 bytes          [emitted] [immutable]
                   js/156.app.035e53999bb6cb9d1b32.js   758 bytes     156  [emitted] [immutable]
                js/156.app.035e53999bb6cb9d1b32.js.br   408 bytes          [emitted] [immutable]
                js/156.app.035e53999bb6cb9d1b32.js.gz   452 bytes          [emitted] [immutable]
                   js/157.app.035e53999bb6cb9d1b32.js   951 bytes     157  [emitted] [immutable]
                js/157.app.035e53999bb6cb9d1b32.js.br   510 bytes          [emitted] [immutable]
                js/157.app.035e53999bb6cb9d1b32.js.gz   558 bytes          [emitted] [immutable]
                   js/158.app.035e53999bb6cb9d1b32.js    2.13 KiB     158  [emitted] [immutable]
                js/158.app.035e53999bb6cb9d1b32.js.br   816 bytes          [emitted] [immutable]
                js/158.app.035e53999bb6cb9d1b32.js.gz   955 bytes          [emitted] [immutable]
                   js/159.app.035e53999bb6cb9d1b32.js    1.73 KiB     159  [emitted] [immutable]
                js/159.app.035e53999bb6cb9d1b32.js.br   757 bytes          [emitted] [immutable]
                js/159.app.035e53999bb6cb9d1b32.js.gz   817 bytes          [emitted] [immutable]
                    js/16.app.035e53999bb6cb9d1b32.js     317 KiB      16  [emitted] [immutable]  admin-config
                 js/16.app.035e53999bb6cb9d1b32.js.br    54.6 KiB          [emitted] [immutable]
                 js/16.app.035e53999bb6cb9d1b32.js.gz    70.5 KiB          [emitted] [immutable]
                   js/160.app.035e53999bb6cb9d1b32.js  1000 bytes     160  [emitted] [immutable]
                js/160.app.035e53999bb6cb9d1b32.js.br   537 bytes          [emitted] [immutable]
                js/160.app.035e53999bb6cb9d1b32.js.gz   580 bytes          [emitted] [immutable]
                   js/161.app.035e53999bb6cb9d1b32.js    1.03 KiB     161  [emitted] [immutable]
                js/161.app.035e53999bb6cb9d1b32.js.br   539 bytes          [emitted] [immutable]
                js/161.app.035e53999bb6cb9d1b32.js.gz   595 bytes          [emitted] [immutable]
                   js/162.app.035e53999bb6cb9d1b32.js   962 bytes     162  [emitted] [immutable]
                js/162.app.035e53999bb6cb9d1b32.js.br   509 bytes          [emitted] [immutable]
                js/162.app.035e53999bb6cb9d1b32.js.gz   559 bytes          [emitted] [immutable]
                   js/163.app.035e53999bb6cb9d1b32.js    1.82 KiB     163  [emitted] [immutable]
                js/163.app.035e53999bb6cb9d1b32.js.br   611 bytes          [emitted] [immutable]
                js/163.app.035e53999bb6cb9d1b32.js.gz   702 bytes          [emitted] [immutable]
                   js/164.app.035e53999bb6cb9d1b32.js    1.53 KiB     164  [emitted] [immutable]
                js/164.app.035e53999bb6cb9d1b32.js.br   612 bytes          [emitted] [immutable]
                js/164.app.035e53999bb6cb9d1b32.js.gz   695 bytes          [emitted] [immutable]
                   js/165.app.035e53999bb6cb9d1b32.js   946 bytes     165  [emitted] [immutable]
                js/165.app.035e53999bb6cb9d1b32.js.br   478 bytes          [emitted] [immutable]
                js/165.app.035e53999bb6cb9d1b32.js.gz   544 bytes          [emitted] [immutable]
                   js/166.app.035e53999bb6cb9d1b32.js    1.13 KiB     166  [emitted] [immutable]
                js/166.app.035e53999bb6cb9d1b32.js.br   545 bytes          [emitted] [immutable]
                js/166.app.035e53999bb6cb9d1b32.js.gz   639 bytes          [emitted] [immutable]
                   js/167.app.035e53999bb6cb9d1b32.js    1.49 KiB     167  [emitted] [immutable]
                js/167.app.035e53999bb6cb9d1b32.js.br   619 bytes          [emitted] [immutable]
                js/167.app.035e53999bb6cb9d1b32.js.gz   709 bytes          [emitted] [immutable]
                   js/168.app.035e53999bb6cb9d1b32.js   964 bytes     168  [emitted] [immutable]
                js/168.app.035e53999bb6cb9d1b32.js.br   489 bytes          [emitted] [immutable]
                js/168.app.035e53999bb6cb9d1b32.js.gz   551 bytes          [emitted] [immutable]
                   js/169.app.035e53999bb6cb9d1b32.js   975 bytes     169  [emitted] [immutable]
                js/169.app.035e53999bb6cb9d1b32.js.br   489 bytes          [emitted] [immutable]
                js/169.app.035e53999bb6cb9d1b32.js.gz   555 bytes          [emitted] [immutable]
                    js/17.app.035e53999bb6cb9d1b32.js    25.7 KiB      17  [emitted] [immutable]  admin-devtools
                 js/17.app.035e53999bb6cb9d1b32.js.br    5.22 KiB          [emitted] [immutable]
                 js/17.app.035e53999bb6cb9d1b32.js.gz    6.09 KiB          [emitted] [immutable]
                   js/170.app.035e53999bb6cb9d1b32.js    1.01 KiB     170  [emitted] [immutable]
                js/170.app.035e53999bb6cb9d1b32.js.br   398 bytes          [emitted] [immutable]
                js/170.app.035e53999bb6cb9d1b32.js.gz   440 bytes          [emitted] [immutable]
                   js/171.app.035e53999bb6cb9d1b32.js   944 bytes     171  [emitted] [immutable]
                js/171.app.035e53999bb6cb9d1b32.js.br   514 bytes          [emitted] [immutable]
                js/171.app.035e53999bb6cb9d1b32.js.gz   569 bytes          [emitted] [immutable]
                   js/172.app.035e53999bb6cb9d1b32.js   794 bytes     172  [emitted] [immutable]
                js/172.app.035e53999bb6cb9d1b32.js.br   433 bytes          [emitted] [immutable]
                js/172.app.035e53999bb6cb9d1b32.js.gz   483 bytes          [emitted] [immutable]
                   js/173.app.035e53999bb6cb9d1b32.js    1.01 KiB     173  [emitted] [immutable]
                js/173.app.035e53999bb6cb9d1b32.js.br   477 bytes          [emitted] [immutable]
                js/173.app.035e53999bb6cb9d1b32.js.gz   527 bytes          [emitted] [immutable]
                   js/174.app.035e53999bb6cb9d1b32.js     1.6 KiB     174  [emitted] [immutable]
                js/174.app.035e53999bb6cb9d1b32.js.br   544 bytes          [emitted] [immutable]
                js/174.app.035e53999bb6cb9d1b32.js.gz   604 bytes          [emitted] [immutable]
                   js/175.app.035e53999bb6cb9d1b32.js    1.31 KiB     175  [emitted] [immutable]
                js/175.app.035e53999bb6cb9d1b32.js.br   539 bytes          [emitted] [immutable]
                js/175.app.035e53999bb6cb9d1b32.js.gz   634 bytes          [emitted] [immutable]
                   js/176.app.035e53999bb6cb9d1b32.js    1.99 KiB     176  [emitted] [immutable]
                js/176.app.035e53999bb6cb9d1b32.js.br   851 bytes          [emitted] [immutable]
                js/176.app.035e53999bb6cb9d1b32.js.gz   992 bytes          [emitted] [immutable]
                   js/177.app.035e53999bb6cb9d1b32.js    1.16 KiB     177  [emitted] [immutable]
                js/177.app.035e53999bb6cb9d1b32.js.br   511 bytes          [emitted] [immutable]
                js/177.app.035e53999bb6cb9d1b32.js.gz   573 bytes          [emitted] [immutable]
                   js/178.app.035e53999bb6cb9d1b32.js   951 bytes     178  [emitted] [immutable]
                js/178.app.035e53999bb6cb9d1b32.js.br   482 bytes          [emitted] [immutable]
                js/178.app.035e53999bb6cb9d1b32.js.gz   540 bytes          [emitted] [immutable]
                   js/179.app.035e53999bb6cb9d1b32.js    1.15 KiB     179  [emitted] [immutable]
                js/179.app.035e53999bb6cb9d1b32.js.br   555 bytes          [emitted] [immutable]
                js/179.app.035e53999bb6cb9d1b32.js.gz   638 bytes          [emitted] [immutable]
                    js/18.app.035e53999bb6cb9d1b32.js    98.5 KiB      18  [emitted] [immutable]  admin-pages
                 js/18.app.035e53999bb6cb9d1b32.js.br    19.2 KiB          [emitted] [immutable]
                 js/18.app.035e53999bb6cb9d1b32.js.gz    22.7 KiB          [emitted] [immutable]
                   js/180.app.035e53999bb6cb9d1b32.js    1.08 KiB     180  [emitted] [immutable]
                js/180.app.035e53999bb6cb9d1b32.js.br   515 bytes          [emitted] [immutable]
                js/180.app.035e53999bb6cb9d1b32.js.gz   593 bytes          [emitted] [immutable]
                   js/181.app.035e53999bb6cb9d1b32.js    1.13 KiB     181  [emitted] [immutable]
                js/181.app.035e53999bb6cb9d1b32.js.br   612 bytes          [emitted] [immutable]
                js/181.app.035e53999bb6cb9d1b32.js.gz   684 bytes          [emitted] [immutable]
                   js/182.app.035e53999bb6cb9d1b32.js    1.11 KiB     182  [emitted] [immutable]
                js/182.app.035e53999bb6cb9d1b32.js.br   552 bytes          [emitted] [immutable]
                js/182.app.035e53999bb6cb9d1b32.js.gz   615 bytes          [emitted] [immutable]
                   js/183.app.035e53999bb6cb9d1b32.js    1.26 KiB     183  [emitted] [immutable]
                js/183.app.035e53999bb6cb9d1b32.js.br   608 bytes          [emitted] [immutable]
                js/183.app.035e53999bb6cb9d1b32.js.gz   721 bytes          [emitted] [immutable]
                   js/184.app.035e53999bb6cb9d1b32.js   996 bytes     184  [emitted] [immutable]
                js/184.app.035e53999bb6cb9d1b32.js.br   487 bytes          [emitted] [immutable]
                js/184.app.035e53999bb6cb9d1b32.js.gz   583 bytes          [emitted] [immutable]
                   js/185.app.035e53999bb6cb9d1b32.js    1.06 KiB     185  [emitted] [immutable]
                js/185.app.035e53999bb6cb9d1b32.js.br   504 bytes          [emitted] [immutable]
                js/185.app.035e53999bb6cb9d1b32.js.gz   599 bytes          [emitted] [immutable]
                   js/186.app.035e53999bb6cb9d1b32.js    1.26 KiB     186  [emitted] [immutable]
                js/186.app.035e53999bb6cb9d1b32.js.br   607 bytes          [emitted] [immutable]
                js/186.app.035e53999bb6cb9d1b32.js.gz   719 bytes          [emitted] [immutable]
                   js/187.app.035e53999bb6cb9d1b32.js    45.9 KiB     187  [emitted] [immutable]
                js/187.app.035e53999bb6cb9d1b32.js.br    12.8 KiB          [emitted] [immutable]
                js/187.app.035e53999bb6cb9d1b32.js.gz    15.5 KiB          [emitted] [immutable]
                    js/19.app.035e53999bb6cb9d1b32.js    83.4 KiB   19, 6  [emitted] [immutable]  admin-pages-echarts
                 js/19.app.035e53999bb6cb9d1b32.js.br    18.9 KiB          [emitted] [immutable]
                 js/19.app.035e53999bb6cb9d1b32.js.gz    21.7 KiB          [emitted] [immutable]
                     js/2.app.035e53999bb6cb9d1b32.js    96.6 KiB       2  [emitted] [immutable]  vendors~about-page~admin-config~admin-devtools~admin-pages~admin-pages-echarts~admin-pages-leaflet~a~ce773236
                  js/2.app.035e53999bb6cb9d1b32.js.br    24.4 KiB          [emitted] [immutable]
                  js/2.app.035e53999bb6cb9d1b32.js.gz      28 KiB          [emitted] [immutable]
                    js/20.app.035e53999bb6cb9d1b32.js    58.1 KiB   20, 7  [emitted] [immutable]  admin-pages-leaflet
                 js/20.app.035e53999bb6cb9d1b32.js.br    11.5 KiB          [emitted] [immutable]
                 js/20.app.035e53999bb6cb9d1b32.js.gz    13.3 KiB          [emitted] [immutable]
                    js/21.app.035e53999bb6cb9d1b32.js     120 KiB      21  [emitted] [immutable]  admin-rules
                 js/21.app.035e53999bb6cb9d1b32.js.br    22.2 KiB          [emitted] [immutable]
                 js/21.app.035e53999bb6cb9d1b32.js.gz    26.1 KiB          [emitted] [immutable]
                    js/22.app.035e53999bb6cb9d1b32.js    5.98 KiB      22  [emitted] [immutable]  admin-schedule
                 js/22.app.035e53999bb6cb9d1b32.js.br    1.87 KiB          [emitted] [immutable]
                 js/22.app.035e53999bb6cb9d1b32.js.gz    2.13 KiB          [emitted] [immutable]
                    js/23.app.035e53999bb6cb9d1b32.js     144 KiB      23  [emitted] [immutable]  analyzer
                 js/23.app.035e53999bb6cb9d1b32.js.br    21.7 KiB          [emitted] [immutable]
                 js/23.app.035e53999bb6cb9d1b32.js.gz    27.3 KiB          [emitted] [immutable]
                    js/24.app.035e53999bb6cb9d1b32.js     129 KiB      24  [emitted] [immutable]  blockly-editor
                 js/24.app.035e53999bb6cb9d1b32.js.br      25 KiB          [emitted] [immutable]
                 js/24.app.035e53999bb6cb9d1b32.js.gz    29.9 KiB          [emitted] [immutable]
                    js/25.app.035e53999bb6cb9d1b32.js    36.8 KiB  25, 48  [emitted] [immutable]  config-parameter
                 js/25.app.035e53999bb6cb9d1b32.js.br    6.62 KiB          [emitted] [immutable]
                 js/25.app.035e53999bb6cb9d1b32.js.gz    7.46 KiB          [emitted] [immutable]
                    js/26.app.035e53999bb6cb9d1b32.js    24.8 KiB      26  [emitted] [immutable]  cronexpression-editor
                 js/26.app.035e53999bb6cb9d1b32.js.br     3.1 KiB          [emitted] [immutable]
                 js/26.app.035e53999bb6cb9d1b32.js.gz    3.69 KiB          [emitted] [immutable]
                    js/27.app.035e53999bb6cb9d1b32.js    29.1 KiB      27  [emitted] [immutable]  habot
                 js/27.app.035e53999bb6cb9d1b32.js.br    7.75 KiB          [emitted] [immutable]
                 js/27.app.035e53999bb6cb9d1b32.js.gz    9.24 KiB          [emitted] [immutable]
                    js/28.app.035e53999bb6cb9d1b32.js    5.78 KiB      28  [emitted] [immutable]  location-picker
                 js/28.app.035e53999bb6cb9d1b32.js.br    1.96 KiB          [emitted] [immutable]
                 js/28.app.035e53999bb6cb9d1b32.js.gz    2.23 KiB          [emitted] [immutable]
                     js/3.app.035e53999bb6cb9d1b32.js     157 KiB       3  [emitted] [immutable]  vendors~admin-pages-leaflet~location-picker~map-page~plan-page~script-editor
                  js/3.app.035e53999bb6cb9d1b32.js.br    41.7 KiB          [emitted] [immutable]
                  js/3.app.035e53999bb6cb9d1b32.js.gz    48.1 KiB          [emitted] [immutable]
                    js/30.app.035e53999bb6cb9d1b32.js    21.4 KiB      30  [emitted] [immutable]  oh-chart-component
                 js/30.app.035e53999bb6cb9d1b32.js.br    5.33 KiB          [emitted] [immutable]
                 js/30.app.035e53999bb6cb9d1b32.js.gz    5.98 KiB          [emitted] [immutable]
                    js/31.app.035e53999bb6cb9d1b32.js    1.34 KiB      31  [emitted] [immutable]  oh-video-videojs
                 js/31.app.035e53999bb6cb9d1b32.js.br   576 bytes          [emitted] [immutable]
                 js/31.app.035e53999bb6cb9d1b32.js.gz   675 bytes          [emitted] [immutable]
                    js/32.app.035e53999bb6cb9d1b32.js    2.78 KiB      32  [emitted] [immutable]  oh-video-webrtc
                 js/32.app.035e53999bb6cb9d1b32.js.br    1.08 KiB          [emitted] [immutable]
                 js/32.app.035e53999bb6cb9d1b32.js.gz    1.27 KiB          [emitted] [immutable]
                    js/33.app.035e53999bb6cb9d1b32.js    42.7 KiB      33  [emitted] [immutable]  profile-page
                 js/33.app.035e53999bb6cb9d1b32.js.br    9.42 KiB          [emitted] [immutable]
                 js/33.app.035e53999bb6cb9d1b32.js.gz    11.4 KiB          [emitted] [immutable]
                    js/34.app.035e53999bb6cb9d1b32.js     760 KiB   34, 7  [emitted] [immutable]  script-editor
                 js/34.app.035e53999bb6cb9d1b32.js.br     159 KiB          [emitted] [immutable]
                 js/34.app.035e53999bb6cb9d1b32.js.gz     212 KiB          [emitted] [immutable]
                    js/35.app.035e53999bb6cb9d1b32.js    99.9 KiB      35  [emitted] [immutable]  setup-wizard
                 js/35.app.035e53999bb6cb9d1b32.js.br    21.5 KiB          [emitted] [immutable]
                 js/35.app.035e53999bb6cb9d1b32.js.gz    25.9 KiB          [emitted] [immutable]
                    js/36.app.035e53999bb6cb9d1b32.js     747 KiB      36  [emitted] [immutable]  vendors~blockly-editor
                 js/36.app.035e53999bb6cb9d1b32.js.br     128 KiB          [emitted] [immutable]
                 js/36.app.035e53999bb6cb9d1b32.js.gz     164 KiB          [emitted] [immutable]
                    js/37.app.035e53999bb6cb9d1b32.js    54.1 KiB      37  [emitted] [immutable]  vendors~canvas-layout
                 js/37.app.035e53999bb6cb9d1b32.js.br    13.9 KiB          [emitted] [immutable]
                 js/37.app.035e53999bb6cb9d1b32.js.gz    16.2 KiB          [emitted] [immutable]
                    js/38.app.035e53999bb6cb9d1b32.js     263 KiB      38  [emitted] [immutable]  vendors~jssip
                 js/38.app.035e53999bb6cb9d1b32.js.br    44.2 KiB          [emitted] [immutable]
                 js/38.app.035e53999bb6cb9d1b32.js.gz    54.9 KiB          [emitted] [immutable]
                    js/39.app.035e53999bb6cb9d1b32.js     239 KiB  39, 74  [emitted] [immutable]  vendors~oh-chart-component
                 js/39.app.035e53999bb6cb9d1b32.js.br    64.1 KiB          [emitted] [immutable]
                 js/39.app.035e53999bb6cb9d1b32.js.gz    75.4 KiB          [emitted] [immutable]
                     js/4.app.035e53999bb6cb9d1b32.js    42.7 KiB       4  [emitted] [immutable]  vendors~admin-pages-leaflet~map-page~script-editor
                  js/4.app.035e53999bb6cb9d1b32.js.br    7.89 KiB          [emitted] [immutable]
                  js/4.app.035e53999bb6cb9d1b32.js.gz    8.93 KiB          [emitted] [immutable]
                    js/40.app.035e53999bb6cb9d1b32.js     557 KiB      40  [emitted] [immutable]  vendors~oh-video-videojs
                 js/40.app.035e53999bb6cb9d1b32.js.br     130 KiB          [emitted] [immutable]
                 js/40.app.035e53999bb6cb9d1b32.js.gz     154 KiB          [emitted] [immutable]
                    js/41.app.035e53999bb6cb9d1b32.js    1.35 MiB      41  [emitted] [immutable]  vendors~swagger
                 js/41.app.035e53999bb6cb9d1b32.js.br     277 KiB          [emitted] [immutable]
                 js/41.app.035e53999bb6cb9d1b32.js.gz     424 KiB          [emitted] [immutable]
                    js/42.app.035e53999bb6cb9d1b32.js    84 bytes      42  [emitted] [immutable]  vendors~swagger-css
                 js/42.app.035e53999bb6cb9d1b32.js.br    86 bytes          [emitted] [immutable]
                 js/42.app.035e53999bb6cb9d1b32.js.gz    87 bytes          [emitted] [immutable]
                    js/43.app.035e53999bb6cb9d1b32.js    30.9 KiB      43  [emitted] [immutable]  vendors~vue-qrcode
                 js/43.app.035e53999bb6cb9d1b32.js.br    9.86 KiB          [emitted] [immutable]
                 js/43.app.035e53999bb6cb9d1b32.js.gz    11.1 KiB          [emitted] [immutable]
                    js/44.app.035e53999bb6cb9d1b32.js    27.8 KiB      44  [emitted] [immutable]  vendors~zwave-network
                 js/44.app.035e53999bb6cb9d1b32.js.br    8.48 KiB          [emitted] [immutable]
                 js/44.app.035e53999bb6cb9d1b32.js.gz     9.4 KiB          [emitted] [immutable]
                    js/45.app.035e53999bb6cb9d1b32.js    1.91 KiB      45  [emitted] [immutable]  zwave-network
                 js/45.app.035e53999bb6cb9d1b32.js.br   870 bytes          [emitted] [immutable]
                 js/45.app.035e53999bb6cb9d1b32.js.gz  1020 bytes          [emitted] [immutable]
                    js/46.app.035e53999bb6cb9d1b32.js     641 KiB      46  [emitted] [immutable]
                 js/46.app.035e53999bb6cb9d1b32.js.br    78.8 KiB          [emitted] [immutable]
                 js/46.app.035e53999bb6cb9d1b32.js.gz     183 KiB          [emitted] [immutable]
                    js/47.app.035e53999bb6cb9d1b32.js   376 bytes      47  [emitted] [immutable]
                 js/47.app.035e53999bb6cb9d1b32.js.br   109 bytes          [emitted] [immutable]
                 js/47.app.035e53999bb6cb9d1b32.js.gz   133 bytes          [emitted] [immutable]
                    js/48.app.035e53999bb6cb9d1b32.js    2.68 KiB      48  [emitted] [immutable]
                 js/48.app.035e53999bb6cb9d1b32.js.br   931 bytes          [emitted] [immutable]
                 js/48.app.035e53999bb6cb9d1b32.js.gz    1.04 KiB          [emitted] [immutable]
                    js/49.app.035e53999bb6cb9d1b32.js   945 bytes      49  [emitted] [immutable]
                 js/49.app.035e53999bb6cb9d1b32.js.br   480 bytes          [emitted] [immutable]
                 js/49.app.035e53999bb6cb9d1b32.js.gz   549 bytes          [emitted] [immutable]
                     js/5.app.035e53999bb6cb9d1b32.js     372 KiB       5  [emitted] [immutable]  vendors~admin-pages-echarts~oh-chart-component~zwave-network
                  js/5.app.035e53999bb6cb9d1b32.js.br     100 KiB          [emitted] [immutable]
                  js/5.app.035e53999bb6cb9d1b32.js.gz     121 KiB          [emitted] [immutable]
                    js/50.app.035e53999bb6cb9d1b32.js    1.25 KiB      50  [emitted] [immutable]
                 js/50.app.035e53999bb6cb9d1b32.js.br   555 bytes          [emitted] [immutable]
                 js/50.app.035e53999bb6cb9d1b32.js.gz   659 bytes          [emitted] [immutable]
                    js/51.app.035e53999bb6cb9d1b32.js    1.14 KiB      51  [emitted] [immutable]
                 js/51.app.035e53999bb6cb9d1b32.js.br   531 bytes          [emitted] [immutable]
                 js/51.app.035e53999bb6cb9d1b32.js.gz   606 bytes          [emitted] [immutable]
                    js/52.app.035e53999bb6cb9d1b32.js    1.12 KiB      52  [emitted] [immutable]
                 js/52.app.035e53999bb6cb9d1b32.js.br   506 bytes          [emitted] [immutable]
                 js/52.app.035e53999bb6cb9d1b32.js.gz   603 bytes          [emitted] [immutable]
                    js/53.app.035e53999bb6cb9d1b32.js   970 bytes      53  [emitted] [immutable]
                 js/53.app.035e53999bb6cb9d1b32.js.br   430 bytes          [emitted] [immutable]
                 js/53.app.035e53999bb6cb9d1b32.js.gz   498 bytes          [emitted] [immutable]
                    js/54.app.035e53999bb6cb9d1b32.js    1.13 KiB      54  [emitted] [immutable]
                 js/54.app.035e53999bb6cb9d1b32.js.br   513 bytes          [emitted] [immutable]
                 js/54.app.035e53999bb6cb9d1b32.js.gz   613 bytes          [emitted] [immutable]
                    js/55.app.035e53999bb6cb9d1b32.js    1.14 KiB      55  [emitted] [immutable]
                 js/55.app.035e53999bb6cb9d1b32.js.br   513 bytes          [emitted] [immutable]
                 js/55.app.035e53999bb6cb9d1b32.js.gz   603 bytes          [emitted] [immutable]
                    js/56.app.035e53999bb6cb9d1b32.js    1.14 KiB      56  [emitted] [immutable]
                 js/56.app.035e53999bb6cb9d1b32.js.br   523 bytes          [emitted] [immutable]
                 js/56.app.035e53999bb6cb9d1b32.js.gz   609 bytes          [emitted] [immutable]
                    js/57.app.035e53999bb6cb9d1b32.js    1.07 KiB      57  [emitted] [immutable]
                 js/57.app.035e53999bb6cb9d1b32.js.br   539 bytes          [emitted] [immutable]
                 js/57.app.035e53999bb6cb9d1b32.js.gz   625 bytes          [emitted] [immutable]
                    js/58.app.035e53999bb6cb9d1b32.js   990 bytes      58  [emitted] [immutable]
                 js/58.app.035e53999bb6cb9d1b32.js.br   520 bytes          [emitted] [immutable]
                 js/58.app.035e53999bb6cb9d1b32.js.gz   576 bytes          [emitted] [immutable]
                    js/59.app.035e53999bb6cb9d1b32.js   980 bytes      59  [emitted] [immutable]  
                 js/59.app.035e53999bb6cb9d1b32.js.br   475 bytes          [emitted] [immutable]
                 js/59.app.035e53999bb6cb9d1b32.js.gz   548 bytes          [emitted] [immutable]
                     js/6.app.035e53999bb6cb9d1b32.js   738 bytes       6  [emitted] [immutable]  chart-page
                  js/6.app.035e53999bb6cb9d1b32.js.br   362 bytes          [emitted] [immutable]
                  js/6.app.035e53999bb6cb9d1b32.js.gz   427 bytes          [emitted] [immutable]
                    js/60.app.035e53999bb6cb9d1b32.js    1.15 KiB      60  [emitted] [immutable]
                 js/60.app.035e53999bb6cb9d1b32.js.br   561 bytes          [emitted] [immutable]
                 js/60.app.035e53999bb6cb9d1b32.js.gz   671 bytes          [emitted] [immutable]
                    js/61.app.035e53999bb6cb9d1b32.js   918 bytes      61  [emitted] [immutable]
                 js/61.app.035e53999bb6cb9d1b32.js.br   463 bytes          [emitted] [immutable]
                 js/61.app.035e53999bb6cb9d1b32.js.gz   523 bytes          [emitted] [immutable]
                    js/62.app.035e53999bb6cb9d1b32.js    1.04 KiB      62  [emitted] [immutable]
                 js/62.app.035e53999bb6cb9d1b32.js.br   523 bytes          [emitted] [immutable]
                 js/62.app.035e53999bb6cb9d1b32.js.gz   586 bytes          [emitted] [immutable]
                    js/63.app.035e53999bb6cb9d1b32.js    1.53 KiB      63  [emitted] [immutable]
                 js/63.app.035e53999bb6cb9d1b32.js.br   578 bytes          [emitted] [immutable]
                 js/63.app.035e53999bb6cb9d1b32.js.gz   674 bytes          [emitted] [immutable]
                    js/64.app.035e53999bb6cb9d1b32.js    2.12 KiB      64  [emitted] [immutable]
                 js/64.app.035e53999bb6cb9d1b32.js.br   566 bytes          [emitted] [immutable]
                 js/64.app.035e53999bb6cb9d1b32.js.gz   650 bytes          [emitted] [immutable]
                    js/65.app.035e53999bb6cb9d1b32.js    1.26 KiB      65  [emitted] [immutable]
                 js/65.app.035e53999bb6cb9d1b32.js.br   663 bytes          [emitted] [immutable]
                 js/65.app.035e53999bb6cb9d1b32.js.gz   718 bytes          [emitted] [immutable]
                    js/66.app.035e53999bb6cb9d1b32.js   760 bytes      66  [emitted] [immutable]
                 js/66.app.035e53999bb6cb9d1b32.js.br   409 bytes          [emitted] [immutable]
                 js/66.app.035e53999bb6cb9d1b32.js.gz   455 bytes          [emitted] [immutable]
                    js/67.app.035e53999bb6cb9d1b32.js    1.06 KiB      67  [emitted] [immutable]
                 js/67.app.035e53999bb6cb9d1b32.js.br   544 bytes          [emitted] [immutable]
                 js/67.app.035e53999bb6cb9d1b32.js.gz   606 bytes          [emitted] [immutable]
                    js/68.app.035e53999bb6cb9d1b32.js    1.47 KiB      68  [emitted] [immutable]
                 js/68.app.035e53999bb6cb9d1b32.js.br   680 bytes          [emitted] [immutable]
                 js/68.app.035e53999bb6cb9d1b32.js.gz   747 bytes          [emitted] [immutable]
                    js/69.app.035e53999bb6cb9d1b32.js  1020 bytes      69  [emitted] [immutable]
                 js/69.app.035e53999bb6cb9d1b32.js.br   461 bytes          [emitted] [immutable]
                 js/69.app.035e53999bb6cb9d1b32.js.gz   550 bytes          [emitted] [immutable]
                     js/7.app.035e53999bb6cb9d1b32.js    9.09 KiB       7  [emitted] [immutable]  map-page
                  js/7.app.035e53999bb6cb9d1b32.js.br    2.55 KiB          [emitted] [immutable]
                  js/7.app.035e53999bb6cb9d1b32.js.gz    2.95 KiB          [emitted] [immutable]
                    js/70.app.035e53999bb6cb9d1b32.js   957 bytes      70  [emitted] [immutable]
                 js/70.app.035e53999bb6cb9d1b32.js.br   541 bytes          [emitted] [immutable]
                 js/70.app.035e53999bb6cb9d1b32.js.gz   569 bytes          [emitted] [immutable]
                    js/71.app.035e53999bb6cb9d1b32.js   981 bytes      71  [emitted] [immutable]
                 js/71.app.035e53999bb6cb9d1b32.js.br   499 bytes          [emitted] [immutable]
                 js/71.app.035e53999bb6cb9d1b32.js.gz   561 bytes          [emitted] [immutable]
                    js/72.app.035e53999bb6cb9d1b32.js   988 bytes      72  [emitted] [immutable]
                 js/72.app.035e53999bb6cb9d1b32.js.br   506 bytes          [emitted] [immutable]
                 js/72.app.035e53999bb6cb9d1b32.js.gz   564 bytes          [emitted] [immutable]
                    js/73.app.035e53999bb6cb9d1b32.js   753 bytes      73  [emitted] [immutable]
                 js/73.app.035e53999bb6cb9d1b32.js.br   392 bytes          [emitted] [immutable]
                 js/73.app.035e53999bb6cb9d1b32.js.gz   442 bytes          [emitted] [immutable]
                    js/74.app.035e53999bb6cb9d1b32.js     1.2 KiB      74  [emitted] [immutable]
                 js/74.app.035e53999bb6cb9d1b32.js.br   622 bytes          [emitted] [immutable]
                 js/74.app.035e53999bb6cb9d1b32.js.gz   673 bytes          [emitted] [immutable]
                    js/75.app.035e53999bb6cb9d1b32.js    1.44 KiB      75  [emitted] [immutable]
                 js/75.app.035e53999bb6cb9d1b32.js.br   525 bytes          [emitted] [immutable]
                 js/75.app.035e53999bb6cb9d1b32.js.gz   637 bytes          [emitted] [immutable]
                    js/76.app.035e53999bb6cb9d1b32.js    1.26 KiB      76  [emitted] [immutable]
                 js/76.app.035e53999bb6cb9d1b32.js.br   579 bytes          [emitted] [immutable]
                 js/76.app.035e53999bb6cb9d1b32.js.gz   698 bytes          [emitted] [immutable]
                    js/77.app.035e53999bb6cb9d1b32.js   940 bytes      77  [emitted] [immutable]
                 js/77.app.035e53999bb6cb9d1b32.js.br   461 bytes          [emitted] [immutable]
                 js/77.app.035e53999bb6cb9d1b32.js.gz   547 bytes          [emitted] [immutable]
                    js/78.app.035e53999bb6cb9d1b32.js   931 bytes      78  [emitted] [immutable]
                 js/78.app.035e53999bb6cb9d1b32.js.br   453 bytes          [emitted] [immutable]
                 js/78.app.035e53999bb6cb9d1b32.js.gz   539 bytes          [emitted] [immutable]
                    js/79.app.035e53999bb6cb9d1b32.js  1020 bytes      79  [emitted] [immutable]
                 js/79.app.035e53999bb6cb9d1b32.js.br   509 bytes          [emitted] [immutable]
                 js/79.app.035e53999bb6cb9d1b32.js.gz   591 bytes          [emitted] [immutable]
                     js/8.app.035e53999bb6cb9d1b32.js      34 KiB       8  [emitted] [immutable]  plan-page
                  js/8.app.035e53999bb6cb9d1b32.js.br    5.96 KiB          [emitted] [immutable]
                  js/8.app.035e53999bb6cb9d1b32.js.gz     6.9 KiB          [emitted] [immutable]
                    js/80.app.035e53999bb6cb9d1b32.js   935 bytes      80  [emitted] [immutable]
                 js/80.app.035e53999bb6cb9d1b32.js.br   456 bytes          [emitted] [immutable]
                 js/80.app.035e53999bb6cb9d1b32.js.gz   545 bytes          [emitted] [immutable]
                    js/81.app.035e53999bb6cb9d1b32.js   924 bytes      81  [emitted] [immutable]
                 js/81.app.035e53999bb6cb9d1b32.js.br   448 bytes          [emitted] [immutable]
                 js/81.app.035e53999bb6cb9d1b32.js.gz   535 bytes          [emitted] [immutable]
                    js/82.app.035e53999bb6cb9d1b32.js  1020 bytes      82  [emitted] [immutable]
                 js/82.app.035e53999bb6cb9d1b32.js.br   509 bytes          [emitted] [immutable]
                 js/82.app.035e53999bb6cb9d1b32.js.gz   590 bytes          [emitted] [immutable]
                    js/83.app.035e53999bb6cb9d1b32.js  1010 bytes      83  [emitted] [immutable]
                 js/83.app.035e53999bb6cb9d1b32.js.br   506 bytes          [emitted] [immutable]
                 js/83.app.035e53999bb6cb9d1b32.js.gz   594 bytes          [emitted] [immutable]
                    js/84.app.035e53999bb6cb9d1b32.js   936 bytes      84  [emitted] [immutable]
                 js/84.app.035e53999bb6cb9d1b32.js.br   457 bytes          [emitted] [immutable]
                 js/84.app.035e53999bb6cb9d1b32.js.gz   545 bytes          [emitted] [immutable]
                    js/85.app.035e53999bb6cb9d1b32.js  1020 bytes      85  [emitted] [immutable]
                 js/85.app.035e53999bb6cb9d1b32.js.br   509 bytes          [emitted] [immutable]
                 js/85.app.035e53999bb6cb9d1b32.js.gz   591 bytes          [emitted] [immutable]
                    js/86.app.035e53999bb6cb9d1b32.js   322 bytes      86  [emitted] [immutable]
                 js/86.app.035e53999bb6cb9d1b32.js.br   204 bytes          [emitted] [immutable]
                 js/86.app.035e53999bb6cb9d1b32.js.gz   243 bytes          [emitted] [immutable]
                    js/87.app.035e53999bb6cb9d1b32.js   964 bytes      87  [emitted] [immutable]
                 js/87.app.035e53999bb6cb9d1b32.js.br   495 bytes          [emitted] [immutable]
                 js/87.app.035e53999bb6cb9d1b32.js.gz   567 bytes          [emitted] [immutable]
                    js/88.app.035e53999bb6cb9d1b32.js   991 bytes      88  [emitted] [immutable]
                 js/88.app.035e53999bb6cb9d1b32.js.br   505 bytes          [emitted] [immutable]
                 js/88.app.035e53999bb6cb9d1b32.js.gz   581 bytes          [emitted] [immutable]
                    js/89.app.035e53999bb6cb9d1b32.js   991 bytes      89  [emitted] [immutable]
                 js/89.app.035e53999bb6cb9d1b32.js.br   505 bytes          [emitted] [immutable]
                 js/89.app.035e53999bb6cb9d1b32.js.gz   585 bytes          [emitted] [immutable]
                     js/9.app.035e53999bb6cb9d1b32.js    15.5 KiB       9  [emitted] [immutable]  vendors~admin-config~admin-pages
                  js/9.app.035e53999bb6cb9d1b32.js.br    4.87 KiB          [emitted] [immutable]
                  js/9.app.035e53999bb6cb9d1b32.js.gz    5.37 KiB          [emitted] [immutable]
                    js/90.app.035e53999bb6cb9d1b32.js   979 bytes      90  [emitted] [immutable]
                 js/90.app.035e53999bb6cb9d1b32.js.br   499 bytes          [emitted] [immutable]
                 js/90.app.035e53999bb6cb9d1b32.js.gz   574 bytes          [emitted] [immutable]
                    js/91.app.035e53999bb6cb9d1b32.js   980 bytes      91  [emitted] [immutable]
                 js/91.app.035e53999bb6cb9d1b32.js.br   507 bytes          [emitted] [immutable]
                 js/91.app.035e53999bb6cb9d1b32.js.gz   581 bytes          [emitted] [immutable]
                    js/92.app.035e53999bb6cb9d1b32.js    1.29 KiB      92  [emitted] [immutable]
                 js/92.app.035e53999bb6cb9d1b32.js.br   625 bytes          [emitted] [immutable]
                 js/92.app.035e53999bb6cb9d1b32.js.gz   682 bytes          [emitted] [immutable]
                    js/93.app.035e53999bb6cb9d1b32.js    1.09 KiB      93  [emitted] [immutable]
                 js/93.app.035e53999bb6cb9d1b32.js.br   548 bytes          [emitted] [immutable]
                 js/93.app.035e53999bb6cb9d1b32.js.gz   599 bytes          [emitted] [immutable]
                    js/94.app.035e53999bb6cb9d1b32.js    1.18 KiB      94  [emitted] [immutable]
                 js/94.app.035e53999bb6cb9d1b32.js.br   506 bytes          [emitted] [immutable]
                 js/94.app.035e53999bb6cb9d1b32.js.gz   582 bytes          [emitted] [immutable]
                    js/95.app.035e53999bb6cb9d1b32.js    1.62 KiB      95  [emitted] [immutable]
                 js/95.app.035e53999bb6cb9d1b32.js.br   713 bytes          [emitted] [immutable]
                 js/95.app.035e53999bb6cb9d1b32.js.gz   785 bytes          [emitted] [immutable]
                    js/96.app.035e53999bb6cb9d1b32.js   984 bytes      96  [emitted] [immutable]
                 js/96.app.035e53999bb6cb9d1b32.js.br   517 bytes          [emitted] [immutable]
                 js/96.app.035e53999bb6cb9d1b32.js.gz   576 bytes          [emitted] [immutable]
                    js/97.app.035e53999bb6cb9d1b32.js   957 bytes      97  [emitted] [immutable]
                 js/97.app.035e53999bb6cb9d1b32.js.br   502 bytes          [emitted] [immutable]
                 js/97.app.035e53999bb6cb9d1b32.js.gz   548 bytes          [emitted] [immutable]
                    js/98.app.035e53999bb6cb9d1b32.js   969 bytes      98  [emitted] [immutable]
                 js/98.app.035e53999bb6cb9d1b32.js.br   508 bytes          [emitted] [immutable]
                 js/98.app.035e53999bb6cb9d1b32.js.gz   557 bytes          [emitted] [immutable]
                    js/99.app.035e53999bb6cb9d1b32.js   994 bytes      99  [emitted] [immutable]
                 js/99.app.035e53999bb6cb9d1b32.js.br   515 bytes          [emitted] [immutable]
                 js/99.app.035e53999bb6cb9d1b32.js.gz   566 bytes          [emitted] [immutable]
                       js/app.035e53999bb6cb9d1b32.js     1.7 MiB      29  [emitted] [immutable]  main
                    js/app.035e53999bb6cb9d1b32.js.br     331 KiB          [emitted] [immutable]
                    js/app.035e53999bb6cb9d1b32.js.gz     429 KiB          [emitted] [immutable]
                                        manifest.json   887 bytes          [emitted]
                      media/oh-sipclient-ringback.mp3    15.2 KiB          [emitted]
                      media/oh-sipclient-ringtone.mp3     280 KiB          [emitted]
precache-manifest.3d5c034b887b1c4de675b2ec11e23d07.js    42.7 KiB          [emitted]
                                res/icons/128x128.png    3.75 KiB          [emitted]
                                res/icons/144x144.png    4.19 KiB          [emitted]
                                res/icons/152x152.png    4.53 KiB          [emitted]
                                res/icons/192x192.png    5.65 KiB          [emitted]
                                res/icons/256x256.png    7.82 KiB          [emitted]
                                res/icons/512x512.png    17.5 KiB          [emitted]
                       res/icons/apple-touch-icon.png    2.78 KiB          [emitted]
                                res/icons/favicon.svg   725 bytes          [emitted]
                                  res/img/basicui.png    17.1 KiB          [emitted]
                                res/img/cometvisu.png    41.3 KiB          [emitted]
                                    service-worker.js   329 bytes          [emitted]
Entrypoint main = css/app.css js/app.035e53999bb6cb9d1b32.js

Build complete.

See the app entry points alone:

With some moving files around the totals are:

$ du -h js css
2.1M    js/br
2.9M    js/gz
9.5M    js/uncompressed
15M     js
124K    css/br
159K    css/gz
897K    css/uncompressed
1.2M    css

I remember trying this with the HABot client but it wasn't working well with the openHAB Cloud reverse proxy (it didn't forward the Content-Encoding header IIRC).

digitaldan commented 1 year ago

@ghys I'll take a look at both use cases.

Regarding compression, for bundled gzip assets, does the client request something like app.js, and then we would look for app.js.gz and if thats found serve it, but indicate Content-Encoding: gzip in the header? Or will the client ask for app.js.gz?

For other file serving using gziped streams, i wonder, does something in our OSGI stack have any concept of this? is there something we could delegate to for this ? If not, its probably simple to implement, if we decide its worth doing.

digitaldan commented 1 year ago

Also i might look at using an eTag instead of the If-Modified header. Seems like it might be easier to deal with.

ghys commented 1 year ago

Regarding compression, for bundled gzip assets, does the client request something like app.js, and then we would look for app.js.gz and if thats found serve it, but indicate Content-Encoding: gzip in the header?

That's it.

For other file serving using gziped streams, i wonder, does something in our OSGI stack have any concept of this? is there something we could delegate to for this

Not sure, but it looks like Jetty has some support indeed. https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/server/handler/gzip/GzipHandler.html also in the DefaultServlet there's some interesting init parameters: https://www.eclipse.org/jetty/javadoc/jetty-9/org/eclipse/jetty/servlet/DefaultServlet.html

precompressed If set to a comma separated list of encoding types (that may be listed in a requests Accept-Encoding header) to file extension mappings to look for and serve. For example: "br=.br,gzip=.gz,bzip2=.bz". If set to a boolean True, then a default set of compressed formats will be used, otherwise no precompressed formats

ghys commented 1 year ago

Update- I've tried adding:

<Set name="gzip">true</Set>
<Set name="etags">true</Set>

to runtime/etc/jetty.xml around here: https://github.com/openhab/openhab-distro/blob/c4a1d03455cff53712d07ef0c86588097f02f1b7/distributions/openhab/src/main/resources/runtime/etc/jetty.xml#L51-L54

And stopped the main UI bundle so that the default servlet is used again for /static, then placed multiple versions in conf/html like so:

-rwxrwxrwx 1 ys ys 622568 Oct 10 01:29 app.css
-rwxrwxrwx 1 ys ys  68118 Oct 10 01:29 app.css.br
-rwxrwxrwx 1 ys ys  88949 Oct 10 01:29 app.css.gz

And indeed the gzip-encoded version is served and a ETag added:

image

On subsequent requests for this file Chrome sends a If-None-Match: W/"+feotJgJHBI+fepNycU9BM--gzip request header and the server responds with a 304 as expected:

image

Note that the gzip paramater is deprecated and precompressedFormats should be used instead but it needs to be configured with an array in the xml. Definitely no need to reinvent the wheel then if we can use those features in the UI servlet.

digitaldan commented 1 year ago

NIce find! looking now at https://git.eclipse.org/c/jetty/org.eclipse.jetty.project.git/tree/jettyservlet/src/main/java/org/eclipse/jetty/servlet/DefaultServlet.java , looks promising!

digitaldan commented 1 year ago

So i have something working based on extending the jetty default servlet, i believe it actually solves caching, serving local gzip assets and byte range support..... and its actually less code then my original servlet .. and it worked the first time i tried running it....so something is certain to be wrong with it :-)

take a look at https://github.com/digitaldan/openhab-webui/blob/33dfddcd42bedb0705206fb449bb9fd0609b799f/bundles/org.openhab.ui/src/main/java/org/openhab/ui/internal/UIServlet.java , i have not tried acutally testing range or gzip files yet, but i believe they will work , i'll play some more with it this week.

ghys commented 1 year ago

I was hoping you'd come up with something like this, if it indeed works that's brilliant! This will give a nice performance boost when loading the UI (note that when the PWA features are available the service worker is likely to do its own caching, but it has to download the whole 11.34MB first - with GZip/Brotli we can bring it down to ~3MB/2.2MB!).

We have to make sure it plays well with reverse proxies in general and openHAB Cloud in particular, like if the response is compressed it should simply forward it to the client with the Content-Encoding header and not try to decompress it. or strip the header. As mentioned above I had some problems with HABot where the content would arrive GZipped but without the header, maybe that's not the case anymore. Same thing for caching-related request & response headers like ETag and If-None-Match, they have to pass through the proxy.

digitaldan commented 1 year ago

@ghys do you have a branch which uses the webpack compression plugin ? I am running my test branch now on my production home system, so far so good, would like to test the gzipped assets. I'll also throw in a big video file in my html folder and see if Safari can stream it ( I believe thats the best way to test byte range support?)

ghys commented 1 year ago

No, but it's quite easy, just install:

$ npm install compression-webpack-plugin@6.0.3 --save-dev

and add this in webpack.config.js before ...(process.env.WEBPACK_ANALYZER ? [ (line 239):

    new CompressionPlugin({
      filename: '[path][base].gz',
      algorithm: 'gzip',
      test: /\.js$|\.css$|\.html$/,
      threshold: 0,
      minRatio: Infinity,
    }),
    new CompressionPlugin({
      filename: '[path][base].br',
      algorithm: 'brotliCompress',
      test: /\.(js|css|html|svg)$/,
      compressionOptions: {
        level: 11,
      },
      threshold: 0,
      minRatio: Infinity,
    }),

(import it with const CompressionPlugin = require('compression-webpack-plugin') at the top)

ghys commented 1 year ago

( I believe thats the best way to test byte range support?)

Yes I suppose! Or you can test with curl on a text file perhaps: https://everything.curl.dev/http/ranges

digitaldan commented 1 year ago

Success! The servlet is successfully serving br and/or gz files automatically as discussed, caching using etags as well as byte range support. I'm running this on my home system, both locally, remote and through myopenHAB and everything is working. The PR is updated with this new servlet if you want to give it a try. The code is fairly simple as well and does not stray much from your original logic of serving resources which is a nice bonus since that has been throughly tested.

I'll play with it more this week to see if any tweaks are needed. I also want to double check we are passing all headers along through myopenhab to ensure optimal caching is happening.

digitaldan commented 1 year ago

Actually, myopenhab is respecting headers and compressed content, so a win there as well!

ghys commented 1 year ago

A huge step forward, unfortunately there seems to be some problems:

  1. This is not a "real" problem, but it appears Jetty is serving a precompressed version (or not) at random, you can see it in the developer tools if you add the Content-Encoding column (right-click on the headers, then it's under Response Headers):

image

I tried to figure out what was the logic by analyzing https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L320 and https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L356 but

It would be better to have some kind of server preference (which seems to be the case: https://github.com/eclipse/jetty.project/commit/aa8597c19e6cb85d614db33a091946ae163a124b) but I haven't made sense of it. It really seems random based on the first resource it finds... Obviously if Brotli is available and supported by the browser it should be preferred.

  1. Actually, myopenhab is respecting headers and compressed content, so a win there as well!

When the content is gzipped it actually seems that the content is sent uncompressed with a Content-Encoding: gzip header, which for obvious reasons fails to decode.

image

This can be confirmed with some curl commands:

(works on local, response is gzipped & ungzipped by curl)

$ curl -k -v https://192.168.1.123:8443/js/app.9e494f674314ce5c7e79.js --compressed
...JS content...

(doesn't work on myopenhab.org)

$ curl -v --user 'myopenhab@domain.com' 'https://home.myopenhab.org/js/app.9e494f674314ce5c7e79.js' --compressed
Enter host password for user 'myopenhab@domain.com':
(...)
> Accept-Encoding: deflate, gzip, br
(...)
< Content-Encoding: gzip
curl: (61) Error while processing content unencoding: incorrect header check

(works by specifying the header manually, the response is said to be gzip-encoded but is actually not)

$ curl -v --user 'myopenhab@domain.com' 'https://home.myopenhab.org/js/app.9e494f674314ce5c7e79.js' -H 'Accept-Encoding: gzip, br'
(...)
> Accept-Encoding: gzip, br
(...)
< Content-Encoding: gzip
...JS content...

One quick & easy way to solve both 1. & 2. would be to only provide Brotli precompressed assets, as there doesn't seem to be any problems with them, they would be served as the only precompressed encoding available, and they have widespread support, worst case scenario the client would be served uncompressed assets.

  1. When you go to a page with an URL ending in `/', like https://localhost:8443/about/ or https://localhost:8443/settings/ and refresh the page, you end up with an error. It seems the servlet redirects to the URL without the last slash:

image

It can probably be tracked to this code: https://github.com/eclipse/jetty.project/blob/jetty-9.4.x/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceService.java#L261

The redirect shouldn't happen and the index.html page should be served instead for those URLs - the UI's router will pick up the server-relative URL and display the proper page. Maybe the code just above (actually send the contents of /index.html as the "welcome" page when the resource is a directory) could solve this?

digitaldan commented 1 year ago

This is not a "real" problem, but it appears Jetty is serving a precompressed version (or not) at random,

Ok, i did see this once as well, but then it didn't happen, figured it was something weirdly cached on my end, but obviously not. I will need to play with this some more. I'll look at the code you referenced to see i can get a better picture of whats going on. Unfortunately this is not a well documented area of jetty.

FYI, If you turn up debug logging you can see how jetty calls for these files starting with the uncompressed version and moving to gz, and br.

07:30:09.941 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /index.html
07:30:09.942 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1178587240/app/index.html
07:30:09.943 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /index.html.br
07:30:09.944 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1178587240/app/index.html.br
07:30:09.945 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /index.html.gz
07:30:09.946 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1178587240/app/index.html.gz

When the content is gzipped it actually seems that the content is sent uncompressed with a Content-Encoding: gzip

Hmm, strange, all my files were served using br encoding, so i did not actually get to see gzip, are you serving those special or out of the static directory? It did load for me using myopenhab, so let me check again, i may have jumped the gun assuming it was working fine.....fortunately i know a guy who can fix things over there if it not proxing right :-)

When you go to a page with an URL ending in `/', like https://localhost:8443/about/ or https://localhost:8443/settings/ and refresh the page, you end up with an error. It seems the servlet redirects to the URL without the last slash:

Yeah, i thought i took care of that, i even have a comment in the code specifying this very thing. So whats strange is that /page/page_6d44bbde74 works if you reload, as does /page/page_6d44bbde74/ , but not /settings or /settings/ . I'll look at the bundle code again, it's probably a slight tweak to the logic.

digitaldan commented 1 year ago

This is not a "real" problem, but it appears Jetty is serving a precompressed version (or not) at random,

Correction, actually i did not see this exactly, but i did notice some strange behavior where sometimes it loads the single app bundle, and sometime it loads all of the fragments shown in your console. I'm not sure how that works with webpack? Does the client eventually need all the app fragments in the js directory, or can it load one big bundle ?

ghys commented 1 year ago

Thanks for the debug log tip, here's a typical refresh of the about page (with the unfortunate redirect):

20:14:04.956 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about/
20:14:04.959 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/index.html
20:14:04.960 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about/.br
20:14:04.962 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:04.964 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about/.gz
20:14:04.966 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:04.974 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about
20:14:04.976 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/index.html
20:14:04.978 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about.br
20:14:04.980 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:04.982 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /about.gz
20:14:04.984 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:05.028 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /css/app.css
20:14:05.028 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/app.9e494f674314ce5c7e79.js
20:14:05.031 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js
20:14:05.031 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css
20:14:05.032 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /css/app.css.br
20:14:05.033 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/app.9e494f674314ce5c7e79.js.br
20:14:05.034 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css.br
20:14:05.034 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js.br
20:14:05.036 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/app.9e494f674314ce5c7e79.js.gz
20:14:05.037 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /css/app.css.gz
20:14:05.038 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/app.9e494f674314ce5c7e79.js.gz
20:14:05.040 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/css/app.css.gz
20:14:05.388 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /service-worker.js
20:14:05.390 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js
20:14:05.392 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /service-worker.js.br
20:14:05.393 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js.br
20:14:05.395 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /service-worker.js.gz
20:14:05.396 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/service-worker.js.gz
20:14:05.409 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo.svg
20:14:05.411 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo.svg
20:14:05.412 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo.svg.br
20:14:05.414 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo.svg.br
20:14:05.416 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo.svg.gz
20:14:05.417 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:05.443 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo-white.svg
20:14:05.444 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo-white.svg
20:14:05.446 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo-white.svg.br
20:14:05.448 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/images/openhab-logo-white.svg.br
20:14:05.449 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /images/openhab-logo-white.svg.gz
20:14:05.452 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:05.477 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /res/icons/favicon.svg
20:14:05.478 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/res/icons/favicon.svg
20:14:05.480 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /res/icons/favicon.svg.br
20:14:05.482 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/res/icons/favicon.svg.br
20:14:05.483 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /res/icons/favicon.svg.gz
20:14:05.486 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning null
20:14:05.579 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/86.app.9e494f674314ce5c7e79.js
20:14:05.581 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js
20:14:05.582 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/86.app.9e494f674314ce5c7e79.js.br
20:14:05.584 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js.br
20:14:05.586 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource: /js/86.app.9e494f674314ce5c7e79.js.gz
20:14:05.587 [DEBUG] [org.openhab.ui.internal.UIServlet    ] - getResource returning bundleresource://217.fwk1864116663/app/js/86.app.9e494f674314ce5c7e79.js.gz

and the network traffic as seen by the browser: image

I don't really see what determines that this app.9e494f674314ce5c7e79.js is served the gzip version instead of the brotli version.

fortunately i know a guy who can fix things over there if it not proxing right :-)

hehe, that's why I told you the details. It really seems that something in the middleware stack (express or nginx?) knows how to decode gzip but not brotli and decides to decode it.

Correction, actually i did not see this exactly, but i did notice some strange behavior where sometimes it loads the single app bundle, and sometime it loads all of the fragments shown in your console.

It depends on whether the service worker has successfully registered (in that case the workbox library will precache all assets, after which you will typically get a "install" or "add to home screen" prompt). Otherwise the assets will be loaded on demand depending on the needs - for example the SIP widget will load some additional assets. If you reload a simple page (for example the about page), the only UI assets that should be loaded will normally be the entry point, that is app.[hash].js and app.css (along with fonts and logos).

digitaldan commented 1 year ago

@ghys I am not seeing 'gz' assets served when 'br' assets are present. I have tried reloading and clearing cache many times as well as using both firefox and chrome. Any thoughts why its happening in your environment and how i might reproduce ?

ghys commented 1 year ago

@digitaldan Yes I think I figured out the symptom, but have no solution yet.

I'm putting all 3 versions of app.9e494f674314ce5c7e79.js (uncompressed, .gz, .br) from yesterday's builds and also the new one following your last commit (works nice btw) in conf/html and try to fetch them with a browser.

For yesterday's build I always get the gz version but for today's I get the br version. If I delete the gz version from yesterday's build I get the uncompressed version (so the br version is completely ignored), likewise if I delete the br version from today's build, the gz version is ignored.

Turns out the critical line of code is this one: https://github.com/eclipse/jetty.project/blob/171b913242ec70d45e24dac65b8444f1cff70114/jetty-server/src/main/java/org/eclipse/jetty/server/ResourceContentFactory.java#L89

In particular: compressedResource.lastModified() >= resource.lastModified() The compressed resource's last modified date has to be after the original resource's.

On yesterday's build this is not the case for the br asset (last modified 2s before the original):

:~/oh3/conf/html/1$ ls -l --full-time
total 2528
-rw-r--r-- 1 ys ys 1797646 2022-10-12 07:37:04.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys  342226 2022-10-12 07:37:02.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys  444575 2022-10-12 07:37:04.000000000 +0200 app.9e494f674314ce5c7e79.js.gz

On today's build the gz asset's last modified time is 4s before the original:

:~/oh3/conf/html/2$ ls -l --full-time
total 2528
-rw-r--r-- 1 ys ys 1797646 2022-10-13 21:13:38.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys  342226 2022-10-13 21:13:38.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys  444575 2022-10-13 21:13:34.000000000 +0200 app.9e494f674314ce5c7e79.js.gz

So that's why they are ignored.

The solution would be to ensure that the compressed assets always have a last modified date after the originals, but I'm not sure how to achieve that yet!

ghys commented 1 year ago

A faster way to check these last modified dates directly from the bundle is to look for the Last-Modified HTTP header:

$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js
HTTP/1.1 200 OK
Vary: Accept-Encoding
Last-Modified: Thu, 13 Oct 2022 19:13:38 GMT
Content-Type: application/javascript
ETag: W/"/q2XjcZHQ00/q2WDhSYG5M"
Accept-Ranges: bytes
Content-Length: 1797646
Server: Jetty(9.4.46.v20220331)

$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js.gz
HTTP/1.1 200 OK
Last-Modified: Thu, 13 Oct 2022 19:13:34 GMT
Content-Type: application/gzip
ETag: W/"KmkRBvzQpdQKmkQhS4SSns"
Accept-Ranges: bytes
Content-Length: 444575
Server: Jetty(9.4.46.v20220331)

$ curl -I http://localhost:8080/js/app.9e494f674314ce5c7e79.js.br
HTTP/1.1 200 OK
Last-Modified: Thu, 13 Oct 2022 19:13:38 GMT
Content-Type: application/brotli
ETag: W/"KmkRBvzQtJEKmkQhS4RupM"
Accept-Ranges: bytes
Content-Length: 342226
Server: Jetty(9.4.46.v20220331)

Any compressed asset older than the original will never be served as described above.

digitaldan commented 1 year ago

In particular: compressedResource.lastModified() >= resource.lastModified()

Nice find! That would have taken me a while to figure out.

I have a couple ideas, but an easy one would be to extend the Resource object when loading out of the bundle, and for the last modified time always return the same date, like the startup time of the bundle, or grab it from maybe the index.html or something well known . So for example

private long startupTime startupTime = System.currentTimeMillis(); // or we get this from a well known file in the bundle. 
....

class CommonTimeResource extends Resource {

        private Resource baseResource;

        public CustomResource(Resource baseResource) {
            this.baseResource = baseResource;
        }

        // return the same time for all bundled files!
        @Override
        public long lastModified() {
            return startupTime;
        }

        @Override
        public boolean isContainedIn(@Nullable Resource r) throws MalformedURLException {
            return baseResource.isContainedIn(r);
        }

        @Override
        public void close() {
            baseResource.close();

        }

        @Override
        public boolean exists() {
            return baseResource.exists();
        }

        @Override
        public boolean isDirectory() {
            return baseResource.isDirectory();
        }

        @Override
        public long length() {
            return baseResource.length();
        }

        @Override
        public URL getURL() {
            return baseResource.getURL();
        }

        @Override
        public File getFile() throws IOException {
            return baseResource.getFile();
        }

        @Override
        public String getName() {
            return baseResource.getName();
        }

        @Override
        public InputStream getInputStream() throws IOException {
            return baseResource.getInputStream();
        }

        @Override
        public ReadableByteChannel getReadableByteChannel() throws IOException {
            return baseResource.getReadableByteChannel();
        }

        @Override
        public boolean delete() throws SecurityException {
            return baseResource.delete();
        }

        @Override
        public boolean renameTo(@Nullable Resource dest) throws SecurityException {
            return baseResource.renameTo(dest);
        }

        @Override
        public String[] list() {
            return baseResource.list();
        }

        @Override
        public Resource addPath(@Nullable String path) throws IOException, MalformedURLException {
            return baseResource.addPath(path);
        }
    }

FYI I have not actually run this yet, so not sure it works. The other option would be to do something during the node build, but i worry about platform specific time issues depending on the build host and OS.

ghys commented 1 year ago

Yep the custom Resource could work! According to https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/net/URLConnection.html#getLastModified() we can return "0 if not known".

The other option would be to do something during the node build, but i worry about platform specific time issues depending on the build host and OS.

Probably won't work because the dates in the .jar are actually all the same:

$ wget https://ci.openhab.org/job/PR-openHAB-WebUI/lastSuccessfulBuild/artifact/bundles/org.openhab.ui/target/org.openhab.ui-3.4.0-SNAPSHOT.jar
$ unzip org.openhab.ui-3.4.0-SNAPSHOT.jar
$ cd app/js
$ ls -l --full-time
-rw-r--r-- 1 ys ys  121180 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys   31439 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys   38034 2022-10-13 18:30:04.000000000 +0200 0.app.9e494f674314ce5c7e79.js.gz
...
-rw-r--r-- 1 ys ys 1797646 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js
-rw-r--r-- 1 ys ys  342226 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js.br
-rw-r--r-- 1 ys ys  444575 2022-10-13 18:30:04.000000000 +0200 app.9e494f674314ce5c7e79.js.gz

I'm not sure where these different last modified times are from, probably from a cache?

digitaldan commented 1 year ago

Give the latest push a try and let me know what you think !

ghys commented 1 year ago

It works well! Good idea to only use the resource wrapper for the bundle resources and not the user static assets (having the real time in the Last-Modified header can help).

image

Note the odd uncompressed assets, this make sense - that's because of the other condition in Jetty's code, the compressed versions are ignored when they're larger in size than the originals, which is good.

ghys commented 1 year ago

Closes #1432. Closes #1171.

digitaldan commented 1 year ago

Thanks!

Note that the issue with openHAB Cloud uncompressing gzip and sending them plain with the wrong header remains

I'll be looking into that this weekend, its not clear if its the Java http client in the cloud binding who makes the final request to the servlet, something in node land, something in nginx and so on......

ghys commented 1 year ago

Great but it's not as critical in the end, a quick & easy suggestion to reproduce:

$ cd $OPENHAB_CONF/html
$ wget http://localhost:8080/css/app.css
$ wget http://localhost:8080/css/app.css.gz
$ curl -I http://localhost:8080/static/app.css --compressed
    ^ should have Content-Encoding: gzip
$ curl http://localhost:8080/static/app.css --compressed
    ^ should work
$ curl -I -u '<myopenhab username>' https://home.myopenhab.org/static/app.css --compressed
    ^ should have Content-Encoding: gzip
$ curl  -u '<myopenhab username>' https://home.myopenhab.org/static/app.css --compressed
curl: (61) Error while processing content unencoding: incorrect header check
digitaldan commented 1 year ago

Yeah, was not sure if we wanted to support compression look up on static files. Its a one or two line change to the logic if indeed we want to support it .

digitaldan commented 1 year ago

Sorry forget my previous comment, i thought you were referencing something else. My hunch is that the Jetty HTTP client in the binding see's the the returned content encoding and decompresses it before returning its byte array, then we are passing it along but still specifying Content-Encoding: gzip. I think its an easy fix (🤞 )