├── .gitignore ├── LICENSE.md ├── README.md ├── build_chrome.sh ├── build_firefox.sh ├── bun.lockb ├── dist └── style.css ├── help.html ├── img ├── blendbyte.png ├── downtime.png ├── github.jpg ├── icon_black_128.png ├── icon_black_16.png ├── icon_black_256.png ├── icon_black_32.png ├── icon_black_512.png ├── icon_black_64.png ├── icon_white_128.png ├── icon_white_16.png ├── icon_white_256.png ├── icon_white_32.png ├── icon_white_512.png ├── icon_white_64.png ├── ok.png └── reload.png ├── inc ├── animation.css ├── background.js ├── background_script.js ├── data.js ├── icinga.js ├── jquery-3.7.1.min.js ├── offscreen.js ├── options.js ├── popup.js └── service_worker.js ├── manifest.chrome.json ├── manifest.firefox.json ├── offscreen.html ├── options.html ├── package.json ├── popup.html ├── sounds ├── dingding.mp3 ├── horn.mp3 └── laser.mp3 └── src └── style.css /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | manifest.json 3 | *.zip 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Daniel Schmitz, https://bashgeek.net 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Icinga Multi Status for web browsers 2 | 3 | ![unnamed](https://github.com/user-attachments/assets/9ffd3408-e18c-4432-9438-37b30b776775) 4 | 5 | Icinga Multi Status helps you to monitor your (multiple or single) Icinga instances with comfortable alert badges and notifications about incidents right within your web browser. 6 | 7 | Get it from the [Chrome Web Store](https://chrome.google.com/webstore/detail/icinga-multi-status/khabbhcojgkibdeipanmiphceeoiijal) 8 | 9 | Get it from the [Mozilla Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/icinga-multi-status-real/) 10 | 11 | ## Features 12 | 13 | - Monitoring of multiple Icinga Instances (Version 1 and 2) using it's JSON data interface and/or API 14 | - Ability to hide/ignore certain host or service patterns and hosts/services with downtimes 15 | - Configurable refresh time for each instance 16 | - Configurable alarm sounds 17 | - Ability to temporarily disable instances without deleting them 18 | - Status-Icon in Toolbar, showing indicator for problems or number of hosts if everything is okay 19 | - Overview Tab showing status of your Icinga instances as well as it's current host or service problems 20 | - Set an host and/or service issue as "Acknowledged" 21 | - Ability to trigger an immediate service recheck 22 | - Hosts Tab showing you all hosts of all Instances with a quick filter to search for a host 23 | - Services Tab showing you all services of all hosts of all Instances with a quick filter to search for a service 24 | - Instant browser/OS notifications about new problems, if enabled 25 | - Interface completely build on TailwindCSS 26 | 27 | ## Support 28 | 29 | - Icinga Classic UI (Requires Version >= 1.8.0), via HTTP or HTTPS 30 | - Icinga 2 API (Requires Version >= 2.4), via HTTPS 31 | 32 | ## How-to for using the [Icinga 2 API](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/) 33 | - Please make sure you enabled and configured the new API in your Icinga 2 installation by either setting it up manually or use the CLI command for the automatic setup, which also creates a root-user for the API with a random password. 34 | ``` 35 | # Run the automatic setup 36 | $ icinga2 api setup 37 | 38 | # Restart the icinga2 service 39 | $ service icinga2 restart 40 | ``` 41 | - You will find the automatically generated API user here: `/etc/icinga2/conf.d/api-users.conf` 42 | - The default port of the Icinga 2 API is `5665`, so the URL to go with would be like: `https://myicingaserver.com:5665/` 43 | - If you use a self-signed certificate (like the one automatically created in the first step) you have to add it to your browsers/OS trusted store before it'll work in the plugin. This can easily be done be visiting the API URL in a browser tab and accept the certificate. 44 | - Currently we require the following API permissions: ```objects/query/host```, ```status/query```, ```objects/query/service``` 45 | - If you want to use the Acknowledgement functionality, we additionally need: ```actions/acknowledge-problem``` 46 | - If you want to use the Reschedule functionality, we additionally need: ```actions/reschedule-check``` 47 | - If you run into any problems, please stick to the official [Icinga 2 API documentation](https://www.icinga.com/docs/icinga2/latest/doc/12-icinga2-api/) 48 | 49 | ## How to generate assets 50 | - Install NodeJS 20 and [Bun](https://bun.sh/) 51 | - `bun install` 52 | - `bun run build` 53 | 54 | ## Credits 55 | 56 | - Daniel Schmitz, [mastodon.social/@bashgeek](https://mastodon.social/@bashgeek) 57 | - Heavily inspired by [IcingaChromedStatus](https://github.com/kepi/IcingaChromedStatus) 58 | 59 | ## License 60 | 61 | This extension is released under [MIT License](https://github.com/bashgeek/icinga-multi-status/blob/master/LICENSE.md). 62 | -------------------------------------------------------------------------------- /build_chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp manifest.chrome.json manifest.json 3 | zip chrome.zip -r * -x "node_modules/*" -x *.zip 4 | -------------------------------------------------------------------------------- /build_firefox.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp manifest.firefox.json manifest.json 3 | zip firefox.zip -r * -x "node_modules/*" -x *.zip 4 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/bun.lockb -------------------------------------------------------------------------------- /help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Icinga Multi Status - Help 6 | 7 | 8 | 9 | 10 |
11 | 16 | 17 |

Help

18 | 19 |

Global Settings

20 |

Some settings act as a global default and can be overridden by specifying them again in the instance settings.

21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 |
SettingDescription
Refresh RateThe interval in which all active Icinga instances will get queried and the plugin data updated.
Default Acknowledge ExpiryIf you use the Acknowledgment functionality, this will define the default expiration for it.
Default Acknowledge PersistentIf you use the Acknowledgment functionality, this will define the default persistent setting for your acknowledgment. Persistent means, the comment for your acknowledgment will remain after the host/service recovers or the acknowledgment expires.
Default Acknowledge StickyIf you use the Acknowledgment functionality, this will define the default sticky setting for your acknowledgment. Sticky means, the acknowledgment will be set until the service or host fully recovers.
Default Acknowledge AuthorIf you use the Acknowledgment functionality, this will define the default author name for your acknowledgment. Can be anything, most likely your name recognized by your co-workers. The comment will be posted with this username.
Play Alarm SoundWhether you want an alarm sound to be played, if any issues are seen. You can select from a few selected alarms. You can click here to listen to all of them.
Repeat Alarm SoundDefines how often you want to repeat the alarm sound.
60 | 61 |

Instance Settings

62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 |
SettingDescription
Icinga Data URLThe absolute URL to your Icinga setup or API. Both HTTPS and HTTP URLs work. Trailing slash is optional.

Example: https://myicingaserver.com/icinga/ or https://myicingaserver.com:5665/
Icingaweb 2 URLThe absolute URL to your Icingaweb2 instance while you use the Icinga 2 API and therefore this is optional. Enables correct linking of hosts and services. Trailing slash is optional.

Example: https://myicingaserver.com/icingaweb2/
Instance TitleA short but recognizable title for your Icinga instance used by this extension.

Example: My Icinga Instance
UsernameIf your Icinga instance requires an authentication, you can place the username here. If you use the Icinga 2 API type, put your API user here.

Example: icingaadmin
PasswordIf your Icinga instance requires an authentication, you can place the password to the above username here. If you use the Icinga 2 API type, put your API password here.

Example: icingaadmin
Default Acknowledge ExpiryIf you use the Acknowledgment functionality, this will define the default expiration for your acknowledgment.
Default Acknowledge PersistentIf you use the Acknowledgment functionality, this will define the default persistent setting for your acknowledgment. Persistent means, the comment for your acknowledgment will remain after the host/service recovers or the acknowledgment expires.
Default Acknowledge StickyIf you use the Acknowledgment functionality, this will define the default sticky setting for your acknowledgment. Sticky means, the acknowledgment will be set until the service or host fully recovers.
Default Acknowledge AuthorIf you use the Acknowledgment functionality, this will define the default author name for your acknowledgment. Can be anything, most likely your name recognized by your co-workers. The comment will be posted with this username.
Hide and ignore hostsRegular Expression to hide/ignore specific hosts from showing up in the monitoring. You won't receive notifications or alerts of matching hosts. They're also not showing up in the overview lists.

Example: web[0-9]+
Hide and ignore servicesRegular Expression to hide/ignore specific services from showing up in the monitoring. You won't receive notifications or alerts of matching services. They're also not showing up in the overview lists.

Example: (imap|ssh)
Ignore acknowledged host & service problemsIf you acknowledge a host or service problem in Icinga, you won't get notified or alerted here if enabled.
Ignore hosts & services with a downtimeIf a scheduled downtime is active for a host or service in Icinga, you won't get notified or alerted here if enabled.
Ignore hosts & services with problems in SOFT stateOnly respects problems in HARD state if enabled.
No notifications for warningsIf enabled, you won't get any notifications for service warnings.
132 | 133 |

How-to for using the Icinga 2 API

134 | 151 | 152 |

Extension Badges

153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 |
Badge LayoutDescription
"..." on mild blue backgroundIcinga Multi Status is refreshing its data by querying your Icinga instances.
Digit on purple backgroundAmount of Icinga instances having problems (Can't connect to Icinga instance)
Digit on pink backgroundAmount of services having UNKNOWN status
Digit on orange backgroundAmount of services having WARNING status
Digit on red backgroundAmount of hosts having DOWN/UNREACHABLE status and services having CRITICAL status
Digit on green backgroundAmount of hosts having UP status (also means everything ok)
187 | 188 |

FAQ

189 | 203 |
204 | 205 | 206 | -------------------------------------------------------------------------------- /img/blendbyte.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/blendbyte.png -------------------------------------------------------------------------------- /img/downtime.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/downtime.png -------------------------------------------------------------------------------- /img/github.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/github.jpg -------------------------------------------------------------------------------- /img/icon_black_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_128.png -------------------------------------------------------------------------------- /img/icon_black_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_16.png -------------------------------------------------------------------------------- /img/icon_black_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_256.png -------------------------------------------------------------------------------- /img/icon_black_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_32.png -------------------------------------------------------------------------------- /img/icon_black_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_512.png -------------------------------------------------------------------------------- /img/icon_black_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_black_64.png -------------------------------------------------------------------------------- /img/icon_white_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_128.png -------------------------------------------------------------------------------- /img/icon_white_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_16.png -------------------------------------------------------------------------------- /img/icon_white_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_256.png -------------------------------------------------------------------------------- /img/icon_white_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_32.png -------------------------------------------------------------------------------- /img/icon_white_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_512.png -------------------------------------------------------------------------------- /img/icon_white_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/icon_white_64.png -------------------------------------------------------------------------------- /img/ok.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/ok.png -------------------------------------------------------------------------------- /img/reload.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/img/reload.png -------------------------------------------------------------------------------- /inc/animation.css: -------------------------------------------------------------------------------- 1 | @keyframes blink { 2 | 50% { 3 | opacity: 0; 4 | } 5 | } 6 | @keyframes blinkHalf { 7 | 50% { 8 | opacity: 0.4; 9 | } 10 | } 11 | @keyframes fade { 12 | from { 13 | filter: alpha(opacity=0); 14 | opacity: 0; 15 | } 16 | to { 17 | filter: alpha(opacity=100); 18 | opacity: 1; 19 | } 20 | } 21 | 22 | .animation-wrapper { margin-top: 30px; } 23 | 24 | .animation-wrapper p, 25 | .animation-wrapper i { 26 | color: #cccccc; 27 | } 28 | 29 | .animation-wrapper ul { 30 | padding-left: 0; 31 | } 32 | 33 | .animation-wrapper { 34 | text-align: center; 35 | width: 100%; 36 | } 37 | 38 | .animation-content { 39 | height: 195px; 40 | margin: auto; 41 | position: relative; 42 | width: 250px; 43 | } 44 | .animation-content aside { 45 | background: #9e9d9d; 46 | height: 175px; 47 | position: absolute; 48 | width: 20px; 49 | z-index: 1; 50 | } 51 | .animation-content aside:nth-child(1) { 52 | right: 5px; 53 | } 54 | .animation-content aside:nth-child(2) { 55 | left: 5px; 56 | } 57 | .animation-content div { 58 | background: #202730; 59 | box-shadow: 3px 0 5px #757c83, -3px 0 5px #757c83; 60 | height: 50px; 61 | margin: 14px auto; 62 | position: relative; 63 | width: 100%; 64 | z-index: 2; 65 | } 66 | .animation-content div span:nth-child(1) { 67 | background: #80b941; 68 | } 69 | .animation-content div span:nth-child(2) { 70 | background: #f49534; 71 | animation: blinkHalf 0.2s linear alternate infinite; 72 | } 73 | .animation-content li { 74 | background: #818993; 75 | display: inline-block; 76 | height: 30px; 77 | left: -10px; 78 | margin: 0 4px; 79 | position: relative; 80 | top: -36px; 81 | width: 14px; 82 | } 83 | .animation-content li:last-child { 84 | background-color: #80b941; 85 | box-shadow: 0 0 15px #63883b; 86 | color: white; 87 | font-family: monospace; 88 | font-size: 14px; 89 | left: initial; 90 | line-height: 30px; 91 | position: absolute; 92 | right: 4px; 93 | text-align: center; 94 | top: 10px; 95 | width: 30px; 96 | } 97 | .animation-content span { 98 | border-radius: 100%; 99 | display: block; 100 | height: 8px; 101 | left: 10px; 102 | margin: 0 0 15px 0; 103 | position: relative; 104 | top: 10px; 105 | width: 8px; 106 | } 107 | .animation-content div:nth-child(3) li:nth-child(1) { 108 | animation: fade 0.7s 1.1s infinite alternate backwards; 109 | } 110 | .animation-content div:nth-child(3) li:nth-child(2) { 111 | animation: fade 1.14s 0.8s infinite alternate backwards; 112 | } 113 | .animation-content div:nth-child(3) li:nth-child(3) { 114 | animation: fade 1.9s 0.2s infinite alternate backwards; 115 | } 116 | .animation-content div:nth-child(3) li:nth-child(4) { 117 | animation: fade 1.7s 0.4s infinite alternate backwards; 118 | } 119 | .animation-content div:nth-child(3) li:nth-child(5) { 120 | animation: fade 2s infinite alternate backwards; 121 | } 122 | .animation-content div:nth-child(3) li:nth-child(6) { 123 | animation: fade 1.3s 0.6s infinite alternate backwards; 124 | } 125 | .animation-content div:nth-child(4) li:nth-child(1) { 126 | animation: fade 1.88s 0.14s infinite alternate backwards; 127 | } 128 | .animation-content div:nth-child(4) li:nth-child(2) { 129 | animation: fade 2.1s infinite alternate backwards; 130 | } 131 | .animation-content div:nth-child(4) li:nth-child(3) { 132 | animation: fade 0.75s 1.2s infinite alternate backwards; 133 | } 134 | .animation-content div:nth-child(4) li:nth-child(4) { 135 | animation: fade 1.58s 0.4s infinite alternate backwards; 136 | } 137 | .animation-content div:nth-child(4) li:nth-child(5) { 138 | animation: fade 1.44s 0.8s infinite alternate backwards; 139 | } 140 | .animation-content div:nth-child(4) li:nth-child(6) { 141 | animation: fade 1.08s 1s infinite alternate backwards; 142 | } 143 | .animation-content div:nth-child(5) li:nth-child(1) { 144 | animation: fade 1.6s 0.3s infinite alternate backwards; 145 | } 146 | .animation-content div:nth-child(5) li:nth-child(2) { 147 | animation: fade 1.2s 0.6s infinite alternate backwards; 148 | } 149 | .animation-content div:nth-child(5) li:nth-child(3) { 150 | animation: fade 0.8s 1.2s infinite alternate backwards; 151 | } 152 | .animation-content div:nth-child(5) li:nth-child(4) { 153 | animation: fade 1.8s 0.2s infinite alternate backwards; 154 | } 155 | .animation-content div:nth-child(5) li:nth-child(5) { 156 | animation: fade 2s infinite alternate backwards; 157 | } 158 | .animation-content div:nth-child(5) li:nth-child(6) { 159 | animation: fade 1.4s 0.68s infinite alternate backwards; 160 | } 161 | .animation-content div:nth-child(5) li:nth-child(7) { 162 | animation: fade 1.11s 1.11s infinite alternate backwards; 163 | } 164 | .animation-content div:nth-child(6) li:nth-child(1) { 165 | animation: fade 1.8s 0.2s infinite alternate backwards; 166 | } 167 | .animation-content div:nth-child(6) li:nth-child(2) { 168 | animation: fade 1.6s 0.4s infinite alternate backwards; 169 | } 170 | .animation-content div:nth-child(6) li:nth-child(3) { 171 | animation: fade 0.8s 1.2s infinite alternate backwards; 172 | } 173 | .animation-content div:nth-child(6) li:nth-child(4) { 174 | animation: fade 1.2s 0.8s infinite alternate backwards; 175 | } 176 | .animation-content div:nth-child(6) li:nth-child(5) { 177 | animation: fade 1s 1s infinite alternate backwards; 178 | } 179 | .animation-content div:nth-child(6) li:nth-child(6) { 180 | animation: fade 2s infinite alternate backwards; 181 | } 182 | .animation-content div:nth-child(7) li:nth-child(1) { 183 | animation: fade 1.6s 0.4s infinite alternate backwards; 184 | } 185 | .animation-content div:nth-child(7) li:nth-child(2) { 186 | animation: fade 1.8s 0.2s infinite alternate backwards; 187 | } 188 | .animation-content div:nth-child(7) li:nth-child(3) { 189 | animation: fade 1.2s 0.8s infinite alternate backwards; 190 | } 191 | .animation-content div:nth-child(7) li:nth-child(4) { 192 | animation: fade 0.8s 1.2s infinite alternate backwards; 193 | } 194 | .animation-content div:nth-child(7) li:nth-child(5) { 195 | animation: fade 1.4s 0.6s infinite alternate backwards; 196 | } 197 | .animation-content div:nth-child(7) li:nth-child(6) { 198 | animation: fade 1s 1s infinite alternate backwards; 199 | } 200 | .animation-content div:nth-child(8) li:nth-child(1) { 201 | animation: fade 2s infinite alternate backwards; 202 | } 203 | .animation-content div:nth-child(8) li:nth-child(2) { 204 | animation: fade 1.6s 0.4s infinite alternate backwards; 205 | } 206 | .animation-content div:nth-child(8) li:nth-child(3) { 207 | animation: fade 1.2s 0.8s infinite alternate backwards; 208 | } 209 | .animation-content div:nth-child(8) li:nth-child(4) { 210 | animation: fade 1s 1s infinite alternate backwards; 211 | } 212 | .animation-content div:nth-child(8) li:nth-child(5) { 213 | animation: fade 1.4s 0.6s infinite alternate backwards; 214 | } 215 | .animation-content div:nth-child(8) li:nth-child(6) { 216 | animation: fade 0.8s 1.2s infinite alternate backwards; 217 | } 218 | -------------------------------------------------------------------------------- /inc/background.js: -------------------------------------------------------------------------------- 1 | const bg = { 2 | init: function () { 3 | icingaData.init(); 4 | 5 | chrome.alarms.onAlarm.removeListener(bg.refreshAlarm); 6 | chrome.alarms.onAlarm.addListener(bg.refreshAlarm); 7 | 8 | bg.refresh(); 9 | }, 10 | 11 | // Refresh method for alarm 12 | refreshAlarm: function (alarm) { 13 | if (alarm.name !== 'icinga-refresh') { 14 | return; 15 | } 16 | 17 | bg.refresh(); 18 | }, 19 | 20 | // Refresh data for all icinga instances 21 | refresh: function () { 22 | stopHeartbeat(); 23 | icingaData.refresh(); 24 | startHeartbeat(); 25 | bg.restartTimer(); 26 | }, 27 | 28 | // Restart refresh timer 29 | restartTimer: function () { 30 | icinga_get_settings(function (settings) { 31 | settings = settings.settings; 32 | 33 | let real_settings = default_settings; 34 | if (settings == null) { 35 | settings = default_settings; 36 | } else { 37 | Object.keys(settings).forEach(function (key) { 38 | real_settings[key] = settings[key]; 39 | }); 40 | settings = real_settings; 41 | } 42 | 43 | let interval_in_seconds = (settings.refresh === undefined) ? 30 : settings.refresh; 44 | let interval_in_minutes = interval_in_seconds / 60; 45 | chrome.alarms.get('icinga-refresh', alarm => { 46 | if (alarm) { 47 | return; 48 | } 49 | 50 | chrome.alarms.create('icinga-refresh', {periodInMinutes: interval_in_minutes}); 51 | }); 52 | }); 53 | }, 54 | }; 55 | 56 | 57 | let heartbeatInterval; 58 | async function runHeartbeat() { 59 | if (typeof chrome !== 'undefined') { 60 | await chrome.storage.local.set({'last-heartbeat': new Date().getTime()}); 61 | } else { 62 | await browser.storage.local.set({'last-heartbeat': new Date().getTime()}); 63 | } 64 | } 65 | async function startHeartbeat() { 66 | // Run the heartbeat once at service worker startup. 67 | runHeartbeat().then(() => { 68 | heartbeatInterval = setInterval(runHeartbeat, 15 * 1000); 69 | }); 70 | } 71 | async function stopHeartbeat() { 72 | clearInterval(heartbeatInterval); 73 | } 74 | 75 | chrome.alarms.create("keep-loaded-alarm-0", {periodInMinutes: 1}); 76 | setTimeout(() => chrome.alarms.create("keep-loaded-alarm-1", {periodInMinutes: 1}), 20000); 77 | setTimeout(() => chrome.alarms.create("keep-loaded-alarm-2", {periodInMinutes: 1}), 40000); 78 | chrome.alarms.onAlarm.addListener(() => { 79 | chrome.storage.local.set({'keep-alive': new Date().getTime()}); 80 | }); 81 | -------------------------------------------------------------------------------- /inc/background_script.js: -------------------------------------------------------------------------------- 1 | bg.init(); 2 | 3 | browser.runtime.onStartup.addListener(() => { 4 | bg.init(); 5 | }); 6 | 7 | browser.runtime.onInstalled.addListener(details => { 8 | if (details.reason === 'install' || details.reason === 'update') { 9 | bg.init(); 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /inc/data.js: -------------------------------------------------------------------------------- 1 | const icingaData = { 2 | data_hosts: {}, 3 | data_raw: [], 4 | checks: [], 5 | 6 | init: function(){ 7 | }, 8 | 9 | refresh: function(){ 10 | icinga_get_instances(function (instances) { 11 | instances = instances.instances; 12 | 13 | if (instances == null) { 14 | instances = []; 15 | } 16 | 17 | if (instances.length) { 18 | icinga_badge('...', 'reload'); 19 | 20 | icingaData.checks = []; 21 | icingaData.data_raw = []; 22 | 23 | for (i = 0; i < instances.length; i++) { 24 | let e = instances[i]; 25 | if (e.active) { 26 | icingaData.checks[i] = false; 27 | icinga_fetch(e.icinga_type, e.url, e.user, e.pass, 'refresh-background', i); 28 | } 29 | } 30 | 31 | setTimeout(function () { 32 | icingaData.refreshDone(); 33 | }, 1000); 34 | } else { 35 | icinga_badge('', 'reload'); 36 | } 37 | }); 38 | }, 39 | 40 | refreshDone: function() { 41 | let all_done = true; 42 | for (let i = 0; i <= icingaData.checks.length; i++) { 43 | if (icingaData.checks[i] === false) { 44 | all_done = false; 45 | break; 46 | } 47 | } 48 | 49 | if (all_done) { 50 | icingaData.checks = []; 51 | icingaData.data_hosts = {}; 52 | 53 | icinga_get_instances(function (instances) { 54 | instances = instances.instances; 55 | 56 | if (instances == null) { 57 | instances = []; 58 | } 59 | 60 | for (let i = 0; i < icingaData.data_raw.length; i++) { 61 | let e = icingaData.data_raw[i]; 62 | 63 | if (typeof e != 'undefined') { 64 | if (!e.error) { 65 | let regexp_hosts, regexp_services; 66 | // RegExp for hiding 67 | if (instances[i].hide_hosts) { 68 | regexp_hosts = new RegExp(instances[i].hide_hosts, 'i'); 69 | } 70 | if (instances[i].hide_services) { 71 | regexp_services = new RegExp(instances[i].hide_services, 'i'); 72 | } 73 | 74 | // Go through all hosts and services and build object 75 | // If set hide hosts or hide services, ignore then 76 | for (let i_h = 0; i_h < e.hosts.length; i_h++) { 77 | let host = e.hosts[i_h]; 78 | 79 | // Check host if regexp 80 | let add_host = false; 81 | if (instances[i].hide_hosts) { 82 | if (!regexp_hosts.test(host.host_name)) { 83 | add_host = true; 84 | } 85 | } else { 86 | add_host = true; 87 | } 88 | 89 | if (add_host) { 90 | icingaData.data_hosts[i + '_' + host.host_name] = { 91 | 'instance': i, 92 | 'name': host.host_name, 93 | 'status': host.status, 94 | 'state_type': host.state_type, 95 | 'down': host.in_scheduled_downtime, 96 | 'ack': host.has_been_acknowledged, 97 | 'notify': host.notifications_enabled, 98 | 'services': {} 99 | }; 100 | } 101 | } 102 | 103 | for (let i_s = 0; i_s < e.services.length; i_s++) { 104 | let service = e.services[i_s]; 105 | 106 | if (icingaData.data_hosts[i + '_' + service.host_name]) { 107 | // Check service if regexp 108 | let add_service = false; 109 | if (instances[i].hide_services) { 110 | if (!regexp_services.test(service.service_description)) { 111 | add_service = true; 112 | } 113 | } else { 114 | add_service = true; 115 | } 116 | 117 | if (add_service) { 118 | icingaData.data_hosts[i + '_' + service.host_name].services[service.service_description] = { 119 | 'name': service.service_display_name, 120 | 'sname': (typeof (service.service_name) !== 'undefined') ? service.service_name : service.service_display_name, 121 | 'status': service.status, 122 | 'state_type': service.state_type, 123 | 'down': service.in_scheduled_downtime, 124 | 'ack': service.has_been_acknowledged, 125 | 'notify': service.notifications_enabled 126 | }; 127 | } 128 | } 129 | } 130 | } 131 | 132 | // Update last status + error 133 | instances[i].error = e.error; 134 | instances[i].status_last = e.text; 135 | } 136 | } 137 | 138 | icinga_set_instances(instances); 139 | 140 | icingaData.data_raw = []; 141 | icingaData.setHosts(); 142 | icinga_check(); 143 | }); 144 | } else { 145 | setTimeout(function () { 146 | icingaData.refreshDone(); 147 | }, 500); 148 | } 149 | }, 150 | 151 | setInstanceData: function(e){ 152 | icingaData.checks[e.instance] = true; 153 | icingaData.data_raw[e.instance] = e; 154 | }, 155 | 156 | setHosts: function(){ 157 | chrome.storage.local.set({'hosts': icingaData.data_hosts, 'last_update': new Date().toLocaleString()}); 158 | }, 159 | 160 | getHosts: function(callback){ 161 | chrome.storage.local.get('hosts', callback); 162 | }, 163 | 164 | getLastUpdate: function(callback){ 165 | chrome.storage.local.get('last_update', callback); 166 | }, 167 | } 168 | -------------------------------------------------------------------------------- /inc/icinga.js: -------------------------------------------------------------------------------- 1 | let alarm_last_played_time = 0; 2 | let alarm_played_last_refresh = false; 3 | let notf_store = []; 4 | 5 | function icinga_check() 6 | { 7 | // Errors 8 | let error_counts = {instance: 0, warnings: 0, downs: 0, unknown: 0}; 9 | 10 | // Check for instance errors 11 | icinga_get_instances(function (instances) { 12 | instances = instances.instances; 13 | 14 | if (instances == null) { 15 | instances = []; 16 | } 17 | 18 | if (instances.length) { 19 | for (i = 0; i < instances.length; i++) { 20 | let e = instances[i]; 21 | if (e.active) { 22 | if (e.error) { 23 | error_counts.instance += 1; 24 | } 25 | } 26 | } 27 | } 28 | 29 | let should_play_alarm = false; 30 | icingaData.getHosts(function(data){ 31 | if (Object.keys(data.hosts).length === 0) { 32 | return; 33 | } 34 | 35 | // Go through hosts 36 | Object.keys(data.hosts).forEach(function(h, h_i){ 37 | let host = data.hosts[h]; 38 | let instance = instances[host.instance]; 39 | 40 | // something is wrong with host != UP && != PENDING 41 | // Possible DOWN UNREACHABLE UP 42 | if (host.status === 'DOWN' || host.status === 'UNREACHABLE') { 43 | // Just if no downtime or acknowledged based on instance settings 44 | if (!(host.down && instance.hide_down) && !(host.ack && instance.hide_ack) && !(host.state_type === 'SOFT' && instance.hide_soft)) { 45 | icinga_notification(host.instance + '_host_' + host.name, 'Host Problem', host.name + ' (' + host.status + ' - ' + host.state_type + ')', instance.title); 46 | should_play_alarm = true; 47 | 48 | error_counts.downs += 1; 49 | } else { 50 | icinga_notification(host.instance + '_host_' + host.name, 'clear'); 51 | } 52 | } else { 53 | icinga_notification(host.instance + '_host_' + host.name, 'clear'); 54 | } 55 | 56 | // check services on host 57 | Object.keys(host.services).forEach(function(s, s_i){ 58 | let service = host.services[s]; 59 | 60 | // Possible OK WARNING UNKNOWN CRITICAL 61 | // Something is wrong with that status 62 | if ((service.status === 'WARNING' || service.status === 'CRITICAL') && (service.state_type === 'HARD' || !instance.hide_soft)) { 63 | // Just if no downtime or acknowledged based on instance settings 64 | if (!(service.down && instance.hide_down) && !(service.ack && instance.hide_ack)) { 65 | // Notification for this service only if the host is not down, warning just when not ignored 66 | if (host.status === 'UP' && (service.status !== 'WARNING' || (service.status === 'WARNING' && !instance.notf_nowarn))) { 67 | icinga_notification(host.instance + '_service_' + host.name + '_' + service.name, 'Service Problem', host.name + ': ' + service.name + ' (' + service.status + ' - ' + service.state_type + ')', instance.title); 68 | should_play_alarm = true; 69 | } 70 | 71 | if (service.status === 'WARNING') { 72 | error_counts.warnings += 1; 73 | } else { 74 | error_counts.downs += 1; 75 | } 76 | } else { 77 | icinga_notification(host.instance + '_service_' + host.name + '_' + service.name, 'clear'); 78 | } 79 | } else { 80 | if (service.status === 'UNKNOWN') { 81 | // Unknown is always worth a count 82 | error_counts.unknown += 1; 83 | } else { 84 | icinga_notification(host.instance + '_service_' + host.name + '_' + service.name, 'clear'); 85 | } 86 | } 87 | }); 88 | }); 89 | 90 | // Badge 91 | if (error_counts.instance > 0) { 92 | icinga_badge('' + error_counts.instance, 'error'); 93 | } else { 94 | if (error_counts.downs > 0) { 95 | icinga_badge('' + error_counts.downs, 'down'); 96 | } else { 97 | if (error_counts.warnings > 0) { 98 | icinga_badge('' + error_counts.warnings, 'warning'); 99 | } else { 100 | icinga_badge('' + Object.keys(data.hosts).length, 'ok'); 101 | } 102 | } 103 | } 104 | 105 | // Alarm 106 | if (should_play_alarm === true) { 107 | icinga_alarm_play(); 108 | } else { 109 | icinga_alarm_clear(); 110 | } 111 | }); 112 | }); 113 | } 114 | 115 | function icinga_alarm_play() 116 | { 117 | icinga_get_settings(function (settings) { 118 | settings = settings.settings; 119 | let real_settings = default_settings; 120 | if (settings == null) { 121 | settings = default_settings; 122 | } else { 123 | Object.keys(settings).forEach(function (key) { 124 | real_settings[key] = settings[key]; 125 | }); 126 | settings = real_settings; 127 | } 128 | 129 | if (settings.alarm_file === "") { 130 | return; 131 | } 132 | 133 | let play = false; 134 | switch (settings.alarm_repeat) { 135 | case '': 136 | // once 137 | if (alarm_played_last_refresh === false) { 138 | play = true; 139 | } 140 | break; 141 | case 'always': 142 | play = true; 143 | break; 144 | default: 145 | if (alarm_last_played_time === 0 || alarm_last_played_time <= Math.floor(Date.now()/1000) - (parseInt(settings.alarm_repeat) * 60)) { 146 | play = true; 147 | } 148 | break; 149 | } 150 | 151 | if (play) { 152 | if (typeof Audio !== 'undefined') { 153 | let sound = new Audio(chrome.runtime.getURL('sounds/'+settings.alarm_file+'.mp3')); 154 | sound.play(); 155 | } else { 156 | playSound(chrome.runtime.getURL('sounds/' + settings.alarm_file + '.mp3')); 157 | } 158 | 159 | alarm_played_last_refresh = true; 160 | alarm_last_played_time = Math.floor(Date.now() / 1000); 161 | } 162 | }); 163 | } 164 | async function playSound(source = 'default.wav', volume = 1) { 165 | await createOffscreen(); 166 | await chrome.runtime.sendMessage({ play: { source, volume } }); 167 | } 168 | async function createOffscreen() { 169 | if (await chrome.offscreen.hasDocument()) return; 170 | await chrome.offscreen.createDocument({ 171 | url: 'offscreen.html', 172 | reasons: ['AUDIO_PLAYBACK'], 173 | justification: 'audio alarm playback on outages' // details for using the API 174 | }); 175 | } 176 | function icinga_alarm_clear() 177 | { 178 | alarm_played_last_refresh = false; 179 | alarm_last_played_time = 0; 180 | } 181 | 182 | function icinga_notification(id, title, message, context) 183 | { 184 | if (title === 'clear') { 185 | // Clear it 186 | chrome.notifications.clear(id, function (id) { 187 | }); 188 | let index = notf_store.indexOf(id); 189 | if (index >= 0) { 190 | notf_store.splice(index, 1); 191 | } 192 | } else { 193 | // Send it 194 | if (!notf_store.includes(id)) { 195 | notf_store.push(id); 196 | chrome.notifications.create(id, { 197 | 'priority': 2, 198 | 'type': 'basic', 199 | 'iconUrl': chrome.runtime.getURL('/img/icon_black_64.png'), 200 | 'title': title + ": " + context, 201 | 'message': message 202 | }, function (id) { 203 | }); 204 | } 205 | } 206 | } 207 | 208 | function icinga_badge(text, color_type) 209 | { 210 | let color; 211 | 212 | switch (color_type) { 213 | case 'reload': 214 | color = [34, 175, 215, 128]; 215 | break; 216 | case 'error': 217 | color = [166, 35, 215, 255]; 218 | break; 219 | case 'ok': 220 | color = [0, 204, 51, 255]; 221 | break; 222 | case 'unknown': 223 | color = [191, 68, 178, 255]; 224 | break; 225 | case 'warning': 226 | color = [255, 165, 0, 255]; 227 | break; 228 | case 'down': 229 | color = [255, 51, 0, 255]; 230 | break; 231 | } 232 | 233 | chrome.action.setBadgeText({text: text}); 234 | chrome.action.setBadgeBackgroundColor({color: color}); 235 | } 236 | 237 | function icinga_fetch(icinga_type, url, username, password, type, instance) 238 | { 239 | url = url.replace(/\/$/, ''); 240 | 241 | let gurl; 242 | switch (icinga_type) { 243 | default: 244 | gurl = url + '/cgi-bin/status.cgi?style=hostservicedetail&jsonoutput'; 245 | break; 246 | case 'icinga2_api': 247 | gurl = url + '/v1/status/IcingaApplication'; 248 | break; 249 | } 250 | 251 | icinga_get_settings(function (settings) { 252 | settings = settings.settings; 253 | let real_settings = default_settings; 254 | 255 | if (settings == null) { 256 | settings = default_settings; 257 | } else { 258 | Object.keys(settings).forEach(function (key) { 259 | real_settings[key] = settings[key]; 260 | }); 261 | settings = real_settings; 262 | } 263 | 264 | fetch(gurl, { headers: new Headers({ 'Authorization': 'Basic '+btoa(username+':'+password)}) }) 265 | .then(async function (res) { 266 | let error, text, icinga_data_host, icinga_data_service, icinga_version; 267 | const json = await res.json(); 268 | 269 | switch (icinga_type) { 270 | default: 271 | switch (res.status) { 272 | default: 273 | error = 'Unknown Error - Was not able to connect to your Icinga instance. Maybe you are using a self-signed SSL certificate that your browser is not trusting (yet)?'; 274 | break; 275 | case 403: 276 | error = 'Error 403 - Forbidden'; 277 | break; 278 | case 404: 279 | error = 'Error 404 - Bad URL'; 280 | break; 281 | case 401: 282 | error = 'Error 401 - Username/Password wrong'; 283 | break; 284 | case 200: 285 | case 204: 286 | // Get Icinga Version 287 | if (!json.cgi_json_version) { 288 | error = 'Invalid Format from Icinga'; 289 | } else { 290 | icinga_version = json.icinga_status.program_version; 291 | icinga_data_service = json.status.service_status; 292 | icinga_data_host = json.status.host_status; 293 | text = 'OK - ' + icinga_data_host.length + ' hosts, ' + icinga_data_service.length + ' services (Icinga ' + icinga_version + ')'; 294 | } 295 | break; 296 | } 297 | 298 | switch (type) { 299 | case 'instance-check': 300 | instance_save_return((error) ? {error: true, text: error} : {error: false, text: text}); 301 | break; 302 | 303 | case 'refresh-background': 304 | icingaData.setInstanceData((error) ? { 305 | error: true, 306 | text: error, 307 | instance: instance 308 | } : { 309 | error: false, 310 | text: text, 311 | instance: instance, 312 | hosts: icinga_data_host, 313 | services: icinga_data_service 314 | }); 315 | break; 316 | } 317 | break; 318 | case 'icinga2_api': 319 | let no_return = false; 320 | 321 | switch (res.status) { 322 | default: 323 | error = 'unknown error (HTTP status '+res.status+')'; 324 | break; 325 | case 403: 326 | error = 'Error 403 - Forbidden'; 327 | break; 328 | case 404: 329 | error = 'Error 404 - Bad URL'; 330 | break; 331 | case 401: 332 | error = 'Error 401 - Username/Password wrong'; 333 | break; 334 | case 200: 335 | case 204: 336 | icinga_version = json.results[0].status.icingaapplication.app.version; 337 | if (!icinga_version) { 338 | error = 'Invalid Format from Icinga'; 339 | } else { 340 | no_return = true; 341 | 342 | const hostsR = await fetch( 343 | url + '/v1/objects/hosts?attrs=display_name&attrs=state&attrs=downtime_depth&attrs=state_type&attrs=acknowledgement&attrs=enable_notifications&attrs=name', 344 | { headers: new Headers({ 'Authorization': 'Basic '+btoa(username+':'+password) }) } 345 | ) 346 | const hosts = await hostsR.json(); 347 | 348 | const servicesR = await fetch( 349 | url + '/v1/objects/services?attrs=display_name&attrs=state&attrs=downtime_depth&attrs=host_name&attrs=state_type&attrs=acknowledgement&attrs=enable_notifications&attrs=name', 350 | { headers: new Headers({ 'Authorization': 'Basic '+btoa(username+':'+password) }) } 351 | ) 352 | const services = await servicesR.json(); 353 | 354 | icinga_data_host = []; 355 | hosts.results.forEach(function (e) { 356 | icinga_data_host.push({ 357 | host_name: e.name, 358 | status: (e.attrs.state === 1) ? 'DOWN' : 'UP', 359 | state_type: (e.attrs.state_type === 1) ? 'HARD' : 'SOFT', 360 | in_scheduled_downtime: (e.attrs.downtime_depth > 0), 361 | has_been_acknowledged: e.attrs.acknowledgement, 362 | notifications_enabled: e.attrs.enable_notifications, 363 | }); 364 | }); 365 | 366 | icinga_data_service = []; 367 | services.results.forEach(function (e) { 368 | let state; 369 | switch (e.attrs.state) { 370 | case 0: 371 | state = 'OK'; 372 | break; 373 | case 1: 374 | state = 'WARNING'; 375 | break; 376 | case 2: 377 | state = 'CRITICAL'; 378 | break; 379 | case 3: 380 | state = 'UNKNOWN'; 381 | break; 382 | } 383 | icinga_data_service.push({ 384 | host_name: e.attrs.host_name, 385 | service_description: e.attrs.display_name, 386 | service_display_name: e.attrs.display_name, 387 | service_name: e.attrs.name, 388 | status: state, 389 | state_type: (e.attrs.state_type === 1) ? 'HARD' : 'SOFT', 390 | in_scheduled_downtime: (e.attrs.downtime_depth > 0), 391 | has_been_acknowledged: e.attrs.acknowledgement, 392 | notifications_enabled: e.attrs.enable_notifications, 393 | }); 394 | }); 395 | 396 | text = 'OK - ' + icinga_data_host.length + ' hosts, ' + icinga_data_service.length + ' services (Icinga ' + icinga_version + ')'; 397 | 398 | switch (type) { 399 | case 'instance-check': 400 | instance_save_return((error) ? { 401 | error: true, 402 | text: error 403 | } : {error: false, text: text}); 404 | break; 405 | 406 | case 'refresh-background': 407 | icingaData.setInstanceData((error) ? { 408 | error: true, 409 | text: error, 410 | instance: instance 411 | } : { 412 | error: false, 413 | text: text, 414 | instance: instance, 415 | hosts: icinga_data_host, 416 | services: icinga_data_service 417 | }); 418 | break; 419 | } 420 | } 421 | break; 422 | } 423 | 424 | if (!no_return) { 425 | switch (type) { 426 | case 'instance-check': 427 | instance_save_return((error) ? {error: true, text: error} : { 428 | error: false, 429 | text: text 430 | }); 431 | break; 432 | 433 | case 'refresh-background': 434 | icingaData.setInstanceData((error) ? { 435 | error: true, 436 | text: error, 437 | instance: instance 438 | } : { 439 | error: false, 440 | text: text, 441 | instance: instance, 442 | hosts: icinga_data_host, 443 | services: icinga_data_service 444 | }); 445 | break; 446 | } 447 | } 448 | break; 449 | } 450 | }) 451 | .catch(function (err) { 452 | switch (type) { 453 | case 'instance-check': 454 | instance_save_return({error: true, text: err}); 455 | break; 456 | 457 | case 'refresh-background': 458 | icingaData.setInstanceData({error: true, text: err}); 459 | break; 460 | } 461 | }); 462 | }); 463 | } 464 | 465 | function icinga_get_instances(callback) 466 | { 467 | // Migrate old instances 468 | try { 469 | if (localStorage.getItem('instances') !== null) { 470 | chrome.storage.local.set({'instances': JSON.parse(localStorage.getItem('instances'))}); 471 | localStorage.setItem('instances_old', localStorage.getItem('instances')); 472 | localStorage.removeItem('instances'); 473 | } 474 | } catch (l) { 475 | } 476 | 477 | chrome.storage.local.get('instances', callback); 478 | } 479 | 480 | function icinga_set_instances(instances) 481 | { 482 | let real_instances = []; 483 | for (i = 0; i < instances.length; i++) { 484 | let e = instances[i]; 485 | if (e != null) { 486 | real_instances.push(e); 487 | } 488 | } 489 | 490 | chrome.storage.local.set({'instances': real_instances}); 491 | } 492 | 493 | const default_settings = { 494 | 'refresh': 60, 495 | 'ack_expire': 0, 496 | 'ack_persistent': 0, 497 | 'ack_sticky': 0, 498 | 'ack_author': 'icinga-multi-status', 499 | 'alarm_file': '', 500 | 'alarm_repeat': '', 501 | }; 502 | 503 | function icinga_get_settings(callback) 504 | { 505 | // Migrate old settings 506 | try { 507 | if (localStorage.getItem('settings') !== null) { 508 | chrome.storage.local.set({'settings': JSON.parse(localStorage.getItem('settings'))}); 509 | localStorage.setItem('settings_old', localStorage.getItem('settings')); 510 | localStorage.removeItem('settings'); 511 | } 512 | } catch (l) { 513 | } 514 | 515 | chrome.storage.local.get('settings', callback); 516 | } 517 | 518 | function icinga_recheck(type, instance_i, host_name, service_name = '') 519 | { 520 | // Get instance settings 521 | icinga_get_instances((instances) => { 522 | let instance = instances.instances[instance_i]; 523 | if (instance.icinga_type !== 'icinga2_api') { 524 | return; 525 | } 526 | 527 | let payload, payload_for; 528 | switch (type) { 529 | case 'host': 530 | payload = {type: "Host", filter: "host.name==\"" + host_name + "\""}; 531 | payload_for = host_name; 532 | break; 533 | case 'service': 534 | payload = { 535 | type: "Service", 536 | filter: "host.name\=\=\"" + host_name + "\" && service.name\=\=\"" + service_name + "\"" 537 | }; 538 | payload_for = host_name + '[' + service_name + ']'; 539 | break; 540 | } 541 | 542 | $.ajax({ 543 | username: instance.user, 544 | password: instance.pass, 545 | global: false, 546 | timeout: 10000, 547 | url: instance.url.replace(/\/$/, '') + '/v1/actions/reschedule-check', 548 | method: 'POST', 549 | data: JSON.stringify(payload), 550 | headers: {'Accept': 'application/json'}, 551 | complete: function (res) { 552 | let downs = $('#popup-tab-overview-downs'); 553 | 554 | if (res.status === 200) { 555 | downs.append(''); 559 | return; 560 | } 561 | 562 | downs.append(''); 566 | $('#popup-tab-overview-downs .alert-error-info').text('(' + parseInt(res.status) + ') ' + JSON.stringify(res)); 567 | } 568 | }); 569 | }); 570 | } 571 | 572 | function icinga_acknowledge(type, instance_i, host_name, service_name = '') 573 | { 574 | // Get instance settings 575 | icinga_get_instances((instances) => { 576 | let instance = instances.instances[instance_i]; 577 | if (instance.icinga_type !== 'icinga2_api') { 578 | return; 579 | } 580 | 581 | icinga_get_settings((settings) => { 582 | let defaults = []; 583 | defaults['ack_expire'] = (instance.ack_expire == -1) ? (settings.ack_expire || default_settings.ack_expire) : instance.ack_expire; 584 | defaults['ack_persistent'] = (instance.ack_persistent == -1) ? (settings.ack_persistent || default_settings.ack_persistent) : instance.ack_persistent; 585 | defaults['ack_sticky'] = (instance.ack_sticky == -1) ? (settings.ack_sticky || default_settings.ack_sticky) : instance.ack_sticky; 586 | defaults['ack_author'] = (instance.ack_author === "") ? (settings.ack_author || default_settings.ack_author) : instance.ack_author; 587 | 588 | let ack_services = $('#ack-services'); 589 | let payload_for; 590 | switch (type) { 591 | case 'host': 592 | payload_for = host_name; 593 | ack_services.prop('checked', false); 594 | ack_services.parents('.checkbox').show(); 595 | break; 596 | case 'service': 597 | payload_for = host_name + '[' + service_name + ']'; 598 | ack_services.parents('.checkbox').hide(); 599 | break; 600 | } 601 | 602 | // Modal 603 | $('#ack-alert').hide(); 604 | $('#modal_ack h5').html('Acknowledge - ' + payload_for); 605 | $('#ack-expire').val(defaults['ack_expire']); 606 | $('#ack-persistent').val(defaults['ack_persistent']); 607 | $('#ack-sticky').val(defaults['ack_sticky']); 608 | $('#ack-author').val(defaults['ack_author']); 609 | $('#modal_ack') 610 | .on('show.bs.modal', () => { 611 | $('body').css('min-height', '600px'); 612 | }) 613 | .on('hidden.bs.modal', () => { 614 | $('body').css('min-height', ''); 615 | }); 616 | document.getElementById('modal_ack').showModal(); 617 | $('#ack-submit').off('click').on('click', () => { 618 | $('#ack-submit').prop('disabled', true); 619 | 620 | let payload = []; 621 | let author = $('#ack-author').val(); 622 | let comment = $('#ack-comment').val(); 623 | let expiry = parseInt(Math.round((Date.now() / 1000)) + $('#ack-expire').val()); 624 | let sticky = $('#ack-sticky').prop('checked'); 625 | let persistent = $('#ack-persistent').prop('checked'); 626 | 627 | switch (type) { 628 | case 'host': 629 | payload.push({ 630 | type: "Host", 631 | filter: "host.name==\"" + host_name + "\"", 632 | author: author, 633 | comment: comment, 634 | expiry: expiry, 635 | sticky: sticky, 636 | persistent: persistent, 637 | notify: true 638 | }); 639 | 640 | if ($('#ack-services').prop('checked')) { 641 | payload.push({ 642 | type: "Service", 643 | filter: "service.state>0 && host.name==\"" + host_name + "\"", 644 | author: author, 645 | comment: comment, 646 | expiry: expiry, 647 | sticky: sticky, 648 | persistent: persistent, 649 | notify: true 650 | }); 651 | } 652 | break; 653 | case 'service': 654 | payload.push({ 655 | type: "Service", 656 | filter: "host.name\=\=\"" + host_name + "\" && service.name\=\=\"" + service_name + "\"", 657 | author: author, 658 | comment: comment, 659 | expiry: expiry, 660 | sticky: sticky, 661 | persistent: persistent, 662 | notify: true 663 | }); 664 | break; 665 | } 666 | 667 | payload.forEach((p) => { 668 | $.ajax({ 669 | username: instance.user, 670 | password: instance.pass, 671 | global: false, 672 | timeout: 10000, 673 | url: instance.url.replace(/\/$/, '') + '/v1/actions/acknowledge-problem', 674 | method: 'POST', 675 | data: JSON.stringify(p), 676 | headers: {'Accept': 'application/json'}, 677 | complete: function (res) { 678 | let ack_alert = $('#ack-alert'); 679 | 680 | if (res.status === 200) { 681 | ack_alert.show().append(''); 685 | setTimeout(() => { 686 | $('#modal_ack').modal('hide'); 687 | }, 2000); 688 | return; 689 | } 690 | 691 | ack_alert.show().append(''); 695 | $('#ack-alert .alert-error-info').text('(' + parseInt(res.status) + ') ' + JSON.stringify(res)); 696 | } 697 | }); 698 | }); 699 | }); 700 | }); 701 | }); 702 | } 703 | -------------------------------------------------------------------------------- /inc/offscreen.js: -------------------------------------------------------------------------------- 1 | // Listen for messages from the extension 2 | chrome.runtime.onMessage.addListener(msg => { 3 | if ('play' in msg) playAudio(msg.play); 4 | }); 5 | 6 | // Play sound with access to DOM APIs 7 | function playAudio({ source, volume }) { 8 | const audio = new Audio(source); 9 | audio.volume = volume; 10 | audio.play(); 11 | } 12 | -------------------------------------------------------------------------------- /inc/options.js: -------------------------------------------------------------------------------- 1 | function instance_modal(instance) 2 | { 3 | $('#instance-alert').text('').hide(); 4 | $('#instance-submit').prop('disabled', false); 5 | 6 | let modal = $('#modal_instance'); 7 | // modal.find('.form-group').removeClass('has-error'); 8 | 9 | if (instance === -1) { 10 | modal.find('#modal_instance_title').text('Add new instance'); 11 | $('#instance-id').val(-1); 12 | 13 | $('#instance-url').val(''); 14 | $('#instance-url-web').val(''); 15 | $('#instance-title').val(''); 16 | $('#instance-user').val(''); 17 | $('#instance-pass').val(''); 18 | $('#instance-icinga-type').val('icinga2_api'); 19 | $('#instance-hide-hosts').val(''); 20 | $('#instance-hide-services').val(''); 21 | $('#instance-hide-ack').prop('checked', false); 22 | $('#instance-hide-down').prop('checked', false); 23 | $('#instance-hide-soft').prop('checked', false); 24 | $('#instance-notf-nowarn').prop('checked', false); 25 | 26 | $('#instance-ack-expire').val(-1); 27 | $('#instance-ack-persistent').val(-1); 28 | $('#instance-ack-sticky').val(-1); 29 | $('#instance-ack-author').val(''); 30 | } else { 31 | icinga_get_instances(function (instances) { 32 | instances = instances.instances; 33 | 34 | if (instances == null) { 35 | instances = []; 36 | } 37 | 38 | let e = instances[instance]; 39 | modal.find('#modal_instance_title').text('Edit instance'); 40 | $('#instance-id').val(instance); 41 | $('#instance-url').val(e.url); 42 | $('#instance-url-web').val(e.url_web); 43 | $('#instance-title').val(e.title); 44 | $('#instance-icinga-type').val(e.icinga_type); 45 | $('#instance-user').val(e.user); 46 | $('#instance-pass').val(e.pass); 47 | $('#instance-hide-hosts').val(e.hide_hosts); 48 | $('#instance-hide-services').val(e.hide_services); 49 | $('#instance-hide-ack').prop('checked', e.hide_ack); 50 | $('#instance-hide-down').prop('checked', e.hide_down); 51 | $('#instance-hide-soft').prop('checked', e.hide_soft); 52 | $('#instance-notf-nowarn').prop('checked', e.notf_nowarn); 53 | $('#instance-ack-expire').val(e.ack_expire || -1); 54 | $('#instance-ack-persistent').val(e.ack_persistent || -1); 55 | $('#instance-ack-sticky').val(e.ack_sticky || -1); 56 | $('#instance-ack-author').val(e.ack_author || ''); 57 | }); 58 | } 59 | 60 | document.getElementById('modal_instance').showModal(); 61 | } 62 | 63 | function instance_modal_delete(instance) 64 | { 65 | let modal = $('#modal_instance_delete'); 66 | icinga_get_instances(function (instances) { 67 | instances = instances.instances; 68 | 69 | if (instances == null) { 70 | instances = []; 71 | } 72 | 73 | let e = instances[instance]; 74 | $('#instance-delete-submit').prop('disabled', false); 75 | $('#instance-delete-id').val(instance); 76 | $('#instance-delete-title').html(e.title); 77 | 78 | document.getElementById('modal_instance_delete').showModal(); 79 | }); 80 | } 81 | 82 | function instance_delete() 83 | { 84 | $('#instance-delete-submit').prop('disabled', true); 85 | 86 | let modal = $('#modal_instance_delete'); 87 | icinga_get_instances(function (instances) { 88 | instances = instances.instances; 89 | 90 | if (instances == null) { 91 | instances = []; 92 | } 93 | 94 | delete instances[$('#instance-delete-id').val()]; 95 | icinga_set_instances(instances); 96 | 97 | $('#instance-delete-alert').removeClass().addClass('alert alert-success').html('Removed instance!').show(); 98 | instance_table_reload(); 99 | setTimeout(function () { 100 | document.getElementById('modal_instance_delete').close(); 101 | }, 2000); 102 | }); 103 | } 104 | 105 | function instance_active(instance) 106 | { 107 | icinga_get_instances(function (instances) { 108 | instances = instances.instances; 109 | 110 | if (instances == null) { 111 | instances = []; 112 | } 113 | 114 | instances[instance].active = $('#instance-table-active-' + instance).prop('checked'); 115 | icinga_set_instances(instances); 116 | instance_table_reload(); 117 | }); 118 | } 119 | 120 | function instance_save() 121 | { 122 | let errors = []; 123 | 124 | let _submit = $('#instance-submit'); 125 | let _url = $('#instance-url'); 126 | let _wurl = $('#instance-url-web'); 127 | let _title = $('#instance-title'); 128 | let _user = $('#instance-user'); 129 | let _pass = $('#instance-pass'); 130 | let _hide_hosts = $('#instance-hide-hosts'); 131 | let _hide_services = $('#instance-hide-services'); 132 | 133 | _submit.prop('disabled', true); 134 | 135 | if (_url.val()) { 136 | let url = new URL(_url.val()); 137 | if (!url.hostname || (url.protocol !== 'http:' && url.protocol !== 'https:')) { 138 | _url.parent().addClass('has-error'); 139 | errors.push('Given URL does not seem a valid HTTP/HTTPS URL.'); 140 | } else { 141 | // OK 142 | _url.parent().removeClass('has-error'); 143 | 144 | if (!_title.val()) { 145 | _title.val(url.hostname); 146 | } 147 | } 148 | } else { 149 | _url.parent().addClass('has-error'); 150 | errors.push('No URL given'); 151 | } 152 | 153 | if (_wurl.val()) { 154 | let url = new URL(_wurl.val()); 155 | if (!url.hostname || (url.protocol !== 'http:' && url.protocol !== 'https:')) { 156 | _wurl.parent().addClass('has-error'); 157 | errors.push('Given URL does not seem a valid HTTP/HTTPS URL.'); 158 | } else { 159 | // OK 160 | _wurl.parent().removeClass('has-error'); 161 | } 162 | } 163 | 164 | if (_title.val()) { 165 | // OK 166 | _title.parent().removeClass('has-error'); 167 | } else { 168 | _title.parent().addClass('has-error'); 169 | errors.push('No instance title given'); 170 | } 171 | 172 | 173 | if ((_user.val() && _pass.val())) { 174 | // OK 175 | _user.parent().removeClass('has-error'); 176 | _pass.parent().removeClass('has-error'); 177 | } else { 178 | if (_user.val() && !_pass.val()) { 179 | // Pass missing 180 | _pass.parent().addClass('has-error'); 181 | _user.parent().removeClass('has-error'); 182 | errors.push('Username given, but password missing'); 183 | } else { 184 | if (!_user.val() && _pass.val()) { 185 | // User missing 186 | _user.parent().addClass('has-error'); 187 | _pass.parent().removeClass('has-error'); 188 | errors.push('Password given, but username missing'); 189 | } 190 | } 191 | } 192 | 193 | if (_hide_hosts.val()) { 194 | try { 195 | test = new RegExp(_hide_hosts.val(), "i"); 196 | } catch (e) { 197 | _hide_hosts.parent().addClass('has-error'); 198 | errors.push('Invalid regexp for hosts given'); 199 | } 200 | 201 | if (typeof test != 'undefined') { 202 | _hide_hosts.parent().removeClass('has-error'); 203 | } 204 | 205 | delete test; 206 | } 207 | 208 | if (_hide_services.val()) { 209 | try { 210 | test = new RegExp(_hide_services.val(), "i"); 211 | } catch (e) { 212 | _hide_services.parent().addClass('has-error'); 213 | errors.push('Invalid regexp for services given'); 214 | } 215 | 216 | if (typeof test != 'undefined') { 217 | _hide_services.parent().removeClass('has-error'); 218 | } 219 | 220 | delete test; 221 | } 222 | 223 | if (errors.length) { 224 | _submit.prop('disabled', false); 225 | 226 | $('#instance-alert').removeClass().addClass('alert alert-error').html('Errors:
' + errors.join('
')).show(); 227 | } else { 228 | $('#instance-alert').removeClass().addClass('alert alert-info').html('Checking...').show(); 229 | 230 | icinga_fetch($('#instance-icinga-type').val(), _url.val(), _user.val(), _pass.val(), 'instance-check'); 231 | } 232 | } 233 | 234 | function instance_save_return(e) 235 | { 236 | if (e.error) { 237 | $('#instance-submit').prop('disabled', false); 238 | $('#instance-alert').removeClass().addClass('alert alert-error').html('Icinga Error:
' + e.text + '
Hint: Please make sure you gave the addon all required permissions to "access data for all websites", otherwise it cannot reach your Icinga instance.').show(); 239 | } else { 240 | icinga_get_instances(function (instances) { 241 | instances = instances.instances; 242 | 243 | if (instances == null) { 244 | instances = []; 245 | } 246 | 247 | let instance_id = $('#instance-id').val(); 248 | if (instance_id !== "-1") { 249 | // Save 250 | instances[instance_id] = { 251 | 'active': instances[instance_id].active, 252 | 'status_last': instances[instance_id].status_last, 253 | 'url': $('#instance-url').val(), 254 | 'url_web': $('#instance-url-web').val(), 255 | 'icinga_type': $('#instance-icinga-type').val(), 256 | 'user': $('#instance-user').val(), 257 | 'pass': $('#instance-pass').val(), 258 | 'title': $('#instance-title').val(), 259 | 'hide_hosts': $('#instance-hide-hosts').val(), 260 | 'hide_services': $('#instance-hide-services').val(), 261 | 'hide_ack': $('#instance-hide-ack').prop('checked'), 262 | 'hide_down': $('#instance-hide-down').prop('checked'), 263 | 'hide_soft': $('#instance-hide-soft').prop('checked'), 264 | 'notf_nowarn': $('#instance-notf-nowarn').prop('checked'), 265 | 'ack_expire': $('#instance-ack-expire').val(), 266 | 'ack_persistent': $('#instance-ack-persistent').val(), 267 | 'ack_sticky': $('#instance-ack-sticky').val(), 268 | 'ack_author': $('#instance-ack-author').val(), 269 | } 270 | } else { 271 | // Add 272 | instances.push({ 273 | 'active': true, 274 | 'status_last': e.text, 275 | 'url': $('#instance-url').val(), 276 | 'url_web': $('#instance-url-web').val(), 277 | 'user': $('#instance-user').val(), 278 | 'pass': $('#instance-pass').val(), 279 | 'icinga_type': $('#instance-icinga-type').val(), 280 | 'title': $('#instance-title').val(), 281 | 'hide_hosts': $('#instance-hide-hosts').val(), 282 | 'hide_services': $('#instance-hide-services').val(), 283 | 'hide_ack': $('#instance-hide-ack').prop('checked'), 284 | 'hide_down': $('#instance-hide-down').prop('checked'), 285 | 'hide_soft': $('#instance-hide-soft').prop('checked'), 286 | 'notf_nowarn': $('#instance-notf-nowarn').prop('checked'), 287 | 'ack_expire': $('#instance-ack-expire').val(), 288 | 'ack_persistent': $('#instance-ack-persistent').val(), 289 | 'ack_sticky': $('#instance-ack-sticky').val(), 290 | 'ack_author': $('#instance-ack-author').val(), 291 | }); 292 | } 293 | 294 | icinga_set_instances(instances); 295 | 296 | $('#instance-alert').removeClass().addClass('alert alert-success').html('Awesome!
' + e.text).show(); 297 | 298 | instance_table_reload(); 299 | 300 | setTimeout(function () { 301 | document.getElementById('modal_instance').close(); 302 | }, 2000); 303 | }); 304 | } 305 | } 306 | 307 | function instance_table_reload() 308 | { 309 | icinga_get_instances(function (instances) { 310 | instances = instances.instances; 311 | 312 | if (instances == null) { 313 | instances = []; 314 | } 315 | 316 | let tab = $('#instances-table'); 317 | tab.find('tbody').empty(); 318 | 319 | if (instances.length === 0) { 320 | tab.find('tbody').html('No instances configured yet - time to add one!'); 321 | } else { 322 | for (i = 0; i < instances.length; i++) { 323 | var e = instances[i]; 324 | tab.find('tbody').append( 325 | '' 326 | + '' + e.title + '' 327 | + '' + e.status_last + '' 328 | + '' 329 | + '
' 330 | + '' 331 | ); 332 | 333 | $('#instance-table-active-' + i).click({i: i}, function (e) { 334 | instance_active(e.data.i); 335 | }); 336 | $('#instance-table-edit-' + i).click({i: i}, function (e) { 337 | instance_modal(e.data.i); 338 | }); 339 | $('#instance-table-delete-' + i).click({i: i}, function (e) { 340 | instance_modal_delete(e.data.i); 341 | }); 342 | } 343 | } 344 | }); 345 | } 346 | 347 | function instance_untab_url() 348 | { 349 | let _url = $('#instance-url'); 350 | let _title = $('#instance-title'); 351 | if (_url.val()) { 352 | let url = new URL(_url.val()); 353 | if (!url.hostname || (url.protocol !== 'http:' && url.protocol !== 'https:')) { 354 | _url.parent().addClass('has-error'); 355 | } else { 356 | _url.parent().removeClass('has-error'); 357 | 358 | if (!_title.val()) { 359 | _title.val(url.hostname); 360 | } 361 | } 362 | } 363 | } 364 | 365 | function settings_reload() 366 | { 367 | icinga_get_settings(function (settings) { 368 | settings = settings.settings; 369 | 370 | let real_settings = default_settings; 371 | if (settings == null) { 372 | settings = default_settings; 373 | } else { 374 | $.each(settings, function (k, v) { 375 | real_settings[k] = v; 376 | }); 377 | settings = default_settings; 378 | } 379 | 380 | $('#settings-refresh').val(settings.refresh); 381 | $('#settings-ack-expire').val(settings.ack_expire); 382 | $('#settings-ack-persistent').val(settings.ack_persistent); 383 | $('#settings-ack-sticky').val(settings.ack_sticky); 384 | $('#settings-ack-author').val(settings.ack_author); 385 | $('#settings-alarm-file').val(settings.alarm_file); 386 | $('#settings-alarm-repeat').val(settings.alarm_repeat); 387 | }); 388 | } 389 | 390 | function settings_save() 391 | { 392 | chrome.storage.local.set({ 393 | 'settings': { 394 | 'refresh': $('#settings-refresh').val(), 395 | 'ack_expire': $('#settings-ack-expire').val(), 396 | 'ack_persistent': $('#settings-ack-persistent').val(), 397 | 'ack_sticky': $('#settings-ack-sticky').val(), 398 | 'ack_author': $('#settings-ack-author').val(), 399 | 'alarm_file': $('#settings-alarm-file').val(), 400 | 'alarm_repeat': $('#settings-alarm-repeat').val(), 401 | } 402 | }); 403 | 404 | setTimeout(() => { 405 | settings_reload(); 406 | $('#settings-submit-confirm').fadeIn().delay(2000).fadeOut(); 407 | }, 100); 408 | } 409 | 410 | $(document).ready(function () { 411 | $('#settings-submit').click(function () { 412 | settings_save(); 413 | }); 414 | $('#instance-submit').click(function () { 415 | instance_save(); 416 | }); 417 | $('#instance-delete-submit').click(function () { 418 | instance_delete(); 419 | }); 420 | $('#instance-button-add').click(function () { 421 | instance_modal(-1); 422 | }); 423 | $('#instance-url').blur(function () { 424 | instance_untab_url(); 425 | }); 426 | instance_table_reload(); 427 | settings_reload(); 428 | }); 429 | 430 | 431 | -------------------------------------------------------------------------------- /inc/popup.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | $('#popup-nav-overview').click(function () { 3 | popup_nav('overview'); 4 | }); 5 | $('#popup-nav-hosts').click(function () { 6 | popup_nav('hosts'); 7 | }); 8 | $('#popup-nav-services').click(function () { 9 | popup_nav('services'); 10 | }); 11 | $('#popup-nav-about').click(function () { 12 | popup_nav('about'); 13 | }); 14 | 15 | popup_nav('overview'); 16 | 17 | $('#popup-tab-hosts-filter').on('keyup', function () { 18 | popup_nav('hosts'); 19 | }); 20 | $('#popup-tab-services-filter').on('keyup', function () { 21 | popup_nav('services'); 22 | }); 23 | 24 | $('.open-options').click(function () { 25 | if (chrome.runtime.openOptionsPage) { 26 | // New way to open options pages, if supported (Chrome 42+). 27 | chrome.runtime.openOptionsPage(); 28 | } else { 29 | // Reasonable fallback. 30 | window.open(chrome.runtime.getURL('options.html')); 31 | } 32 | }); 33 | }); 34 | 35 | const badge_classes = { 36 | 'UP': 'badge-success', 37 | 'DOWN': 'badge-error', 38 | 'UNREACHABLE': 'badge-info', 39 | 'OK': 'badge-success', 40 | 'WARNING': 'badge-warning', 41 | 'UNKNOWN': 'badge-info', 42 | 'CRITICAL': 'badge-error' 43 | }; 44 | 45 | function popup_nav(to) 46 | { 47 | $('#popup-nav a').each(function () { 48 | $(this).removeClass('tab-active'); 49 | }); 50 | $('#popup-nav-' + to).addClass('tab-active'); 51 | 52 | $('.popup-tab').each(function () { 53 | $(this).hide(); 54 | }); 55 | $('#popup-tab-' + to).show(); 56 | 57 | let return_func; 58 | switch (to) { 59 | case 'services': 60 | return_func = function (e, last_update) { 61 | $('#popup-tab-services-tables').empty(); 62 | 63 | icinga_get_instances(function (instances) { 64 | instances = instances.instances; 65 | 66 | if (instances == null) { 67 | instances = []; 68 | } 69 | 70 | // Go through active instances 71 | for (let i = 0; i < instances.length; i++) { 72 | let instance = instances[i]; 73 | let instance_line = ''; 74 | 75 | if (instance.active) { 76 | // Go through all hosts and check instance 77 | $.each(Object.keys(e.hosts).sort(), function (h_i, h) { 78 | let host = e.hosts[h]; 79 | if (host.instance === i) { 80 | // Host HTML Line 81 | let host_name; 82 | switch (instance.icinga_type) { 83 | default: 84 | host_name = '' + host.name + ''; 85 | break; 86 | case 'icinga2_api': 87 | if (instance.url_web) { 88 | host_name = '' + host.name + ''; 89 | } else { 90 | host_name = host.name; 91 | } 92 | break; 93 | } 94 | let host_line = '' + host_name + '' + ""+host.status+"" + ''; 95 | let service_line = ''; 96 | 97 | // Go through all services 98 | $.each(Object.keys(host.services).sort(), function (s_i, s) { 99 | let service = host.services[s]; 100 | 101 | let add_service = false; 102 | if ($('#popup-tab-services-filter').val()) { 103 | let reg = new RegExp($('#popup-tab-services-filter').val(), "i"); 104 | if (reg.test(service.name)) { 105 | add_service = true; 106 | } 107 | } else { 108 | add_service = true; 109 | } 110 | 111 | // Service HTML Line 112 | if (add_service) { 113 | let service_name; 114 | switch (instance.icinga_type) { 115 | default: 116 | service_name = '' + service.name + ''; 117 | break; 118 | case 'icinga2_api': 119 | if (instance.url_web) { 120 | service_name = '' + service.name + ''; 121 | } else { 122 | service_name = service.name; 123 | } 124 | break; 125 | } 126 | service_line += '' + service_name + '' 127 | + ""+service.status+"" 128 | + (service.ack ? ' ' : '') 129 | + (service.down ? ' ' : '') 130 | + ''; 131 | } 132 | }); 133 | 134 | // If there is a service line OR host status down, add host line and append service lines 135 | if (service_line !== '') { 136 | instance_line += host_line + service_line; 137 | } 138 | } 139 | }); 140 | 141 | // Insert table 142 | if (instance_line) { 143 | $('#popup-tab-services-tables').append('' 144 | + '
' + instance.title + '
' 145 | + '' 146 | + '' 147 | + '' 148 | + '' 149 | + '' 150 | + '' 151 | //+ '' 152 | + '' 153 | + '' 154 | + '' 155 | + instance_line 156 | + '' 157 | + '
HostServiceStatus
' 158 | ); 159 | } 160 | } 161 | } 162 | }); 163 | } 164 | break; 165 | 166 | case 'hosts': 167 | return_func = function (e, last_update) { 168 | $('#popup-tab-hosts-tables').empty(); 169 | 170 | icinga_get_instances(function (instances) { 171 | instances = instances.instances; 172 | 173 | if (instances == null) { 174 | instances = []; 175 | } 176 | 177 | // Go through active instances 178 | let instance_line; 179 | for (let i = 0; i < instances.length; i++) { 180 | let instance = instances[i]; 181 | instance_line = ''; 182 | 183 | if (instance.active) { 184 | // Go through all hosts and check instance 185 | $.each(Object.keys(e.hosts).sort(), function (h_i, h) { 186 | let host = e.hosts[h]; 187 | if (host.instance === i) { 188 | // regexp check filter 189 | let add_host = false; 190 | if ($('#popup-tab-hosts-filter').val()) { 191 | let reg = new RegExp($('#popup-tab-hosts-filter').val(), "i"); 192 | if (reg.test(host.name)) { 193 | add_host = true; 194 | } 195 | } else { 196 | add_host = true; 197 | } 198 | 199 | if (add_host) { 200 | // Host HTML Line 201 | let host_name; 202 | switch (instance.icinga_type) { 203 | default: 204 | host_name = '' + host.name + ''; 205 | break; 206 | case 'icinga2_api': 207 | if (instance.url_web) { 208 | host_name = '' + host.name + ''; 209 | } else { 210 | host_name = host.name; 211 | } 212 | break; 213 | } 214 | let host_line = '' + host_name + '' 215 | + ""+host.status+"" 216 | + (host.ack ? ' ' : '') 217 | + (host.down ? ' ' : '') 218 | + ''; 219 | instance_line += host_line; 220 | } 221 | } 222 | }); 223 | 224 | // Insert table 225 | if (instance_line) { 226 | $('#popup-tab-hosts-tables').append('' 227 | + '
' + instance.title + '
' 228 | + '' 229 | + '' 230 | + '' 231 | + '' 232 | + '' 233 | + '' 234 | + '' 235 | + '' 236 | + instance_line 237 | + '' 238 | + '
HostStatus
' 239 | ); 240 | } 241 | } 242 | } 243 | }); 244 | }; 245 | break; 246 | 247 | case 'overview': 248 | return_func = function (e, last_update) { 249 | console.log(e, last_update) 250 | 251 | let ov_downs = $('#popup-tab-overview-downs'); 252 | ov_downs.empty(); 253 | 254 | $('#popup-tab-overview-table tbody').empty(); 255 | $('#popup-tab-overview .alert').hide(); 256 | 257 | icinga_get_instances(function (instances) { 258 | instances = instances.instances; 259 | 260 | if (instances == null) { 261 | instances = []; 262 | } 263 | 264 | let worst_status = 0; 265 | 266 | // Go through active instances 267 | for (let i = 0; i < instances.length; i++) { 268 | let instance = instances[i]; 269 | let instance_i = i; 270 | let counter_total = {hosts: 0, services: 0}; 271 | let counter_hosts = {up: 0, down: 0, unr: 0}; 272 | let counter_services = {ok: 0, warn: 0, unkn: 0, crit: 0}; 273 | 274 | if (instance.active && !instance.error) { 275 | let instance_lines = []; 276 | 277 | // Go through all hosts and check instance 278 | $.each(Object.keys(e.hosts), function (h_i, h) { 279 | let host = e.hosts[h]; 280 | if (host.instance === i) { 281 | counter_total.hosts += 1; 282 | 283 | // Possible DOWN UNREACHABLE UP 284 | switch (host.status) { 285 | case 'UP': 286 | counter_hosts.up += 1; 287 | break; 288 | case 'DOWN': 289 | counter_hosts.down += 1; 290 | break; 291 | case 'UNREACHABLE': 292 | counter_hosts.unr += 1; 293 | break; 294 | } 295 | 296 | // Host HTML Line 297 | let host_line = $(''); 298 | let service_lines = []; 299 | let host_options = null; 300 | switch (instance.icinga_type) { 301 | default: 302 | host_line.append($('').html('' + host.name + '')); 303 | break; 304 | case 'icinga2_api': 305 | host_line.append($('').html( 306 | instance.url_web 307 | ? '' + host.name + '' 308 | : host.name 309 | )); 310 | host_options = $('').append($('
') 311 | .append( 312 | $('
32 | 33 | 34 | 35 |
36 |
37 |

Global Settings

38 | ? 39 |
40 |
41 | 56 | 78 | 87 | 96 | 102 | 113 | 127 |
128 | 129 |
130 | 131 | 132 |
133 |
134 | 135 | 136 | 137 | 267 | 268 | 269 | 270 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "tailwindcss -i ./src/style.css -o ./dist/style.css --watch", 6 | "build": "tailwindcss -i ./src/style.css -o ./dist/style.css" 7 | }, 8 | "devDependencies": { 9 | "daisyui": "^5.0.0-beta.8", 10 | "tailwindcss": "^4.0.7", 11 | "@tailwindcss/cli": "^4.0.7" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Icinga Multi Status - Popp 6 | 7 | 8 | 9 | 10 | 11 |
12 | 18 |
19 | 20 |
21 |
22 | 23 | 139 | 143 | 147 | 172 | 173 | 174 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | -------------------------------------------------------------------------------- /sounds/dingding.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/sounds/dingding.mp3 -------------------------------------------------------------------------------- /sounds/horn.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/sounds/horn.mp3 -------------------------------------------------------------------------------- /sounds/laser.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bashgeek/icinga-multi-status/628fa4272db63bdcd9a78869c40e05bb904ac755/sounds/laser.mp3 -------------------------------------------------------------------------------- /src/style.css: -------------------------------------------------------------------------------- 1 | @import 'tailwindcss'; 2 | @plugin "daisyui" { 3 | themes: light --default, dark --prefersdark; 4 | } 5 | 6 | @source '**.{js,css,html}'; 7 | @source 'inc/**.{js,css}'; 8 | --------------------------------------------------------------------------------