├── .editorconfig ├── .gitignore ├── .hintrc ├── LICENSE ├── README-development.md ├── README.md ├── config-overrides.js ├── create-tar-for-github.sh ├── index.html ├── node ├── README.txt ├── app.js ├── package-lock.json ├── package.json ├── settings-nagios.dist.js ├── settings.dist.js └── start.sh ├── package-lock.json ├── package.json ├── proxy.js ├── public ├── apple-touch-icon.png ├── auto-version-switch.php ├── autoupdate.sh ├── connectors │ ├── livestatus-settings.ini.sample │ └── livestatus.php ├── favicon.ico ├── favicon.png ├── icon-1024.png ├── icon-180.png ├── icon-256.png ├── js │ └── polyfill.min.js ├── manifest.json ├── multi-nagios-server-example.html ├── sample-audio │ ├── critical.mp3 │ ├── ok.mp3 │ └── warning.mp3 ├── sample-data │ ├── alertlist.json │ ├── commentlist.json │ ├── hostcount.json │ ├── hostlist.json │ ├── programstatus.json │ ├── servicecount.json │ └── servicelist.json ├── sample-image │ ├── nagios.png │ └── resedit.png └── save-client-settings.php ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── atoms │ ├── alertAtom.ts │ ├── commentlistAtom.ts │ ├── hostAtom.ts │ ├── hostgroupAtom.js │ ├── programAtom.ts │ ├── serviceAtom.ts │ ├── settingsState.ts │ └── skipVersionAtom.ts ├── components │ ├── AppContext.tsx │ ├── Base.css │ ├── Base.tsx │ ├── Dashboard.css │ ├── Dashboard.tsx │ ├── DashboardFetch.tsx │ ├── Demo.css │ ├── Demo.tsx │ ├── Doomguy │ │ ├── Doomguy.css │ │ ├── Doomguy.png │ │ └── Doomguy.tsx │ ├── Help.css │ ├── Help.tsx │ ├── Settings.css │ ├── Settings.tsx │ ├── SettingsFakeData.tsx │ ├── SettingsLoad.tsx │ ├── Update.css │ ├── Update.tsx │ ├── alerts │ │ ├── AlertFilters.css │ │ ├── AlertFilters.tsx │ │ ├── AlertItem.css │ │ ├── AlertItem.tsx │ │ ├── AlertItems.css │ │ ├── AlertItems.tsx │ │ ├── AlertSection.css │ │ ├── AlertSection.tsx │ │ ├── QuietFor.css │ │ └── QuietFor.tsx │ ├── animation.css │ ├── hosts │ │ ├── HostFilters.css │ │ ├── HostFilters.tsx │ │ ├── HostGroupFilter.css │ │ ├── HostGroupFilter.tsx │ │ ├── HostItem.css │ │ ├── HostItem.tsx │ │ ├── HostItems.css │ │ ├── HostItems.tsx │ │ ├── HostSection.tsx │ │ └── host-functions.ts │ ├── panels │ │ ├── BottomPanel.css │ │ ├── BottomPanel.tsx │ │ ├── LeftPanel.css │ │ ├── LeftPanel.tsx │ │ ├── RightPanel.css │ │ ├── RightPanel.tsx │ │ ├── TopPanel.css │ │ └── TopPanel.tsx │ ├── services │ │ ├── ServiceFilters.css │ │ ├── ServiceFilters.tsx │ │ ├── ServiceGroupFilter.css │ │ ├── ServiceGroupFilter.tsx │ │ ├── ServiceItem.css │ │ ├── ServiceItem.tsx │ │ ├── ServiceItems.css │ │ ├── ServiceItems.tsx │ │ ├── ServiceSection.tsx │ │ └── service-functions.ts │ ├── summary │ │ ├── MostRecentAlert.tsx │ │ ├── Summary.css │ │ └── Summary.tsx │ └── widgets │ │ ├── Clock.css │ │ ├── Clock.tsx │ │ ├── CustomLogo.css │ │ ├── CustomLogo.tsx │ │ ├── FilterCheckbox.css │ │ ├── FilterCheckbox.tsx │ │ ├── HistoryChart.css │ │ ├── HistoryChart.tsx │ │ ├── HowMany.css │ │ ├── HowMany.tsx │ │ ├── HowManyEmoji.css │ │ ├── HowManyEmoji.tsx │ │ ├── MiniMapCanvas.css │ │ ├── MiniMapCanvas.tsx │ │ ├── MiniMapWrap.tsx │ │ ├── PollingSpinner.css │ │ ├── PollingSpinner.tsx │ │ ├── Progress.css │ │ ├── Progress.tsx │ │ ├── ScrollToSection.tsx │ │ ├── ScrollToTop.css │ │ └── ScrollToTop.tsx ├── helpers │ ├── audio.ts │ ├── axios.ts │ ├── colors.ts │ ├── date-math.ts │ ├── dates.tsx │ ├── language.ts │ ├── languages │ │ ├── french.ts │ │ └── spanish.ts │ ├── nagios.ts │ └── nagiostv.ts ├── index.css ├── index.tsx ├── logo.svg ├── settingsLoader.js ├── types │ ├── commentTypes.ts │ ├── hostAndServiceTypes.ts │ └── settings.ts └── vite-env.d.ts ├── tsconfig.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | indent_style = tab 8 | indent_size = 4 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = false 12 | insert_final_newline = false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | /releases 4 | /public/autoupdate 5 | /public/client-settings.json 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # Node.js server 28 | /node/node_modules 29 | /node/settings.js 30 | /node/settings-nagios.js 31 | 32 | # some of my scripts 33 | deploy-to-bigwood.sh 34 | deploy-to-nagiostv-demo.sh -------------------------------------------------------------------------------- /.hintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "development" 4 | ], 5 | "hints": { 6 | "axe/forms": "off" 7 | } 8 | } -------------------------------------------------------------------------------- /README-development.md: -------------------------------------------------------------------------------- 1 | ## Development - Setup 2 | ------------ 3 | 4 | ### Requirements 5 | - Git 6 | - Node.js 7 | 8 | 9 | ### Setup Instruxtions 10 | ```console 11 | $ git clone https://github.com/chriscareycode/nagiostv-react.git 12 | $ cd nagiostv-react 13 | $ npm install 14 | $ npm start 15 | ``` 16 | 17 | Make sure you can access your web server on the hostname and port shown, and you can start editing files. 18 | 19 | **However**, you _probably_ see error messages since you _likely_ don't have a Nagios data source. 20 | You'll either want to use the provided _Mock Data_ or _proxy server_. 21 | 22 | 23 | 24 | ### Using Mock data 25 | As of version 0.3.2, mock data is included for doing local development. Without connecting to a real Nagios server, the UI will simulate one of each type of outage. This eliminates the need for the proxy if you just want to make some quick changes. To turn this on: 26 | - add `?fakedata=true` to the URL 27 | - _-or-_ set **useFakeSampleData = true** in `Base.jsx` 28 | 29 | ### Using Live Data 30 | To use Live Data from your actual Nagios server, edit and run the included **proxy server** like this: 31 | - Edit `proxy.js` using the instructions there 32 | - Run `npm run proxy` in a terminal 33 | - Change your NagiosTV development **Nagios cgi-bin path** setting to use whatever that says. That path will probably be `http://localhost:8080/nagios/cgi-bin/` 34 | 35 | Voila! You should see live data populate soon (or refresh it) 36 | 37 | #### More info on proxying (if you want to roll your own) 38 | The scripts will not be able to access the Nagios CGIs since, by default, Nagios does not enable CORS headers on those scripts, and the Nagios cgi-bin path may also 39 | have authentication enabled. You will need to either modify 40 | your Apache install to add CORS headers there, or to run a simple Node.js server or Apache config that will proxy the request and add the CORS headers and auth. 41 | 42 | ### Demo Mode 43 | Demo Mode uses the fakes/mock data and simulates events happening, just as used on the [demo site](https://nagiostv.com/demo/). 44 | 45 | You can also enable "Demo Mode" by adding `?demo=true` to the URL. 46 | 47 | Development - Committing your changes to this project 48 | ------------ 49 | - Fork the project 50 | - Create a feature branch and do your changes there 51 | - Push your feature branch up to origin 52 | - Submit a Pull Request 53 | 54 | ## Memory profiling 55 | 56 | More recently we are moving from React Classes to React Functional (with Hooks) 57 | and switching some state management to Recoil. 58 | As of the time of writing 2021-10-03, when in development mode, Recoil will leak memory 59 | into `window.$recoilDebugStates`. You can set window.$recoilDebugStates = [] before memory profiling to try to get around this 60 | https://github.com/facebookexperimental/Recoil/issues/471#issuecomment-685217406 -------------------------------------------------------------------------------- /config-overrides.js: -------------------------------------------------------------------------------- 1 | /* config-overrides.js */ 2 | 3 | // for react fast refresh 4 | const { override } = require('customize-cra') 5 | const { addReactRefresh } = require('customize-cra-react-refresh') 6 | 7 | module.exports = override(addReactRefresh()) 8 | -------------------------------------------------------------------------------- /create-tar-for-github.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | npm run build 3 | 4 | # remove settings that should not be in the build 5 | rm build/client-settings.json 6 | rm build/connectors/livestatus-settings.ini 7 | rm build/node/settings-nagios.js 8 | 9 | mkdir -p releases/nagiostv 10 | rm releases/nagiostv-0.0.0.tar.gz 11 | rm -rf releases/nagiostv/* 12 | rsync -av --delete build/* releases/nagiostv 13 | cd releases 14 | tar -zcvf nagiostv-0.0.0.tar.gz nagiostv 15 | cd .. 16 | echo . 17 | echo Now rename releases/nagiostv-0.0.0.tar.gz to the new version name 18 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | NagiosTV 19 | 20 | 21 | 22 | 23 | 26 |
27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /node/README.txt: -------------------------------------------------------------------------------- 1 | This is an example Node.js proxy that can be used for local development. 2 | If you are just running the NagiosTV application and not doing local development, this is not neccesary. 3 | 4 | Requirements: 5 | 6 | Node.js 7 | 8 | Initial setup 9 | 10 | $ npm install 11 | $ cp settings.dist.js settings.js 12 | $ cp settings-nagios.dist.js settings-nagios.js 13 | 14 | (Edit settings-nagios.js with your Nagios server settings) 15 | 16 | Running the server: 17 | 18 | $ node app.js 19 | 20 | Then open up NagiosTV and change the Nagios cgi-bin path to the configuration it gives you. -------------------------------------------------------------------------------- /node/app.js: -------------------------------------------------------------------------------- 1 | //============================================================================= 2 | // Requires 3 | //============================================================================= 4 | 5 | var express = require('express'); 6 | var app = express(); 7 | var cors = require('cors'); 8 | var fs = require('fs'); 9 | var bodyParser = require('body-parser'); 10 | // express-http-proxy https://github.com/villadora/express-http-proxy 11 | var proxy = require('express-http-proxy'); 12 | 13 | //============================================================================= 14 | // Settings 15 | //============================================================================= 16 | 17 | let settings; 18 | let settingsNagios; 19 | 20 | // Load the settings.js file, if it exists 21 | try { 22 | const stats = fs.lstatSync('settings.js'); 23 | if (stats.isFile()) { console.log('settings.js file found. This is where the Node.js server settings are stored.'); } 24 | settings = require('./settings'); 25 | } 26 | catch (e) { 27 | console.log('****************************************************************************************************************'); 28 | console.log('No settings.js file found. This is where the Node.js server settings are stored.') 29 | console.log('Copy the file settings.dist.js to settings.js and edit settings.js if you want to. The settings.js file will not be overwritten by updates.'); 30 | console.log('****************************************************************************************************************'); 31 | process.exit(); 32 | } 33 | 34 | loadSettingsNagios(); 35 | 36 | //============================================================================= 37 | // loadSettings and saveSettings 38 | //============================================================================= 39 | function loadSettingsNagios() { 40 | // Load the settings-nagios.js file, if it exists 41 | try { 42 | const stats = fs.lstatSync('settings-nagios.js'); 43 | if (stats.isFile()) { console.log('settings-nagios.js file found. This is where the Nagios server config is set.'); } 44 | settingsNagios = require('./settings-nagios'); 45 | console.log('Nagios Server: ' + settingsNagios.nagiosServerHost); 46 | } catch (e) { 47 | console.log('****************************************************************************************************************'); 48 | console.log('No settings-nagios.js found. This is where the webUI will store it\'s settings, once you save them to the server.'); 49 | console.log('You can copy the file settings-nagios.dist.js to settings-nagios.js and edit the file manually. The settings-nagios.js file will not be overwritten by updates.'); 50 | console.log('****************************************************************************************************************'); 51 | } 52 | } 53 | 54 | //============================================================================= 55 | // Set up routes 56 | //============================================================================= 57 | 58 | app.use(cors()); 59 | 60 | // to support JSON-encoded bodies 61 | app.use(bodyParser.json()); 62 | 63 | // to support URL-encoded bodies 64 | app.use(bodyParser.urlencoded({ 65 | extended: true 66 | })); 67 | 68 | app.use('/', express.static('../dist')); 69 | 70 | //*********************************************************************** 71 | //* Start Proxy 72 | //*********************************************************************** 73 | 74 | let proxyUrl = ''; 75 | if (settingsNagios) { proxyUrl = settingsNagios.nagiosServerHost + settingsNagios.nagiosServerCgiPath; } 76 | console.log('Will proxy requests to ' + proxyUrl); 77 | 78 | var proxyOptions = { 79 | proxyReqPathResolver: function(req) { 80 | if (settings.debug) { console.log('Proxying to URL: ' + proxyUrl + '/' + req.params.resource); } 81 | //return require('url').parse(req.url).path; 82 | var url = require('url').parse(req.url); 83 | //console.log('proxy-' + url.path + '?' + url.query); 84 | //console.log(req.params); 85 | return proxyUrl + '/' + req.params.resource + '?' + url.query; 86 | //return url.path + '?' + url.query; 87 | }, 88 | proxyReqOptDecorator: function(proxyReqOpts, originalReq) { 89 | proxyReqOpts.rejectUnauthorized = false 90 | return proxyReqOpts; 91 | } 92 | }; 93 | 94 | // Add auth if it is enabled 95 | if (settingsNagios && settingsNagios.auth) { 96 | proxyOptions.headers = { 97 | Authorization: "Basic " + new Buffer.from(settingsNagios.username + ':' + settingsNagios.password).toString('base64') 98 | }; 99 | //console.log('Adding proxy auth'); 100 | //console.log(proxyOptions); 101 | } 102 | 103 | app.get('/nagios/:resource', proxy(proxyUrl + '/:resource', proxyOptions)); 104 | 105 | //*********************************************************************** 106 | //* End Proxy 107 | //*********************************************************************** 108 | 109 | // Server listen on port 110 | app.listen(settings.serverPort); 111 | 112 | console.log('Listening on port ' + settings.serverPort + '...'); 113 | console.log(' '); 114 | console.log(`This server will proxy and add auth to the Nagios server at`); 115 | console.log(`${settingsNagios.nagiosServerHost}${settingsNagios.nagiosServerCgiPath}`); 116 | console.log(' '); 117 | console.log(`In NagiosTV settings you can now set the Nagios cgi-bin path to:`); 118 | console.log(`http://:${settings.serverPort}/nagios/`); 119 | console.log(`http://localhost:${settings.serverPort}/nagios/`); 120 | console.log(' '); 121 | -------------------------------------------------------------------------------- /node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nagiostv-proxy", 3 | "description": "REST API with Node", 4 | "version": "0.0.1", 5 | "private": true, 6 | "dependencies": { 7 | "body-parser": "^1.20.2", 8 | "cors": "^2.8.5", 9 | "express": "^4.21.2", 10 | "express-http-proxy": "^2.0.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /node/settings-nagios.dist.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "nagiosServerHost": "http://10.69.0.19", 3 | "nagiosServerCgiPath": "/nagios/cgi-bin", 4 | "auth": true, 5 | "username": "nagiosadmin", 6 | "password": "change-this-to-the-password" 7 | } -------------------------------------------------------------------------------- /node/settings.dist.js: -------------------------------------------------------------------------------- 1 | /***** 2 | * Configurable Settings for the Node.js server 3 | *****/ 4 | 5 | module.exports = { 6 | 7 | serverPort: 4000, 8 | debug: false 9 | 10 | }; 11 | -------------------------------------------------------------------------------- /node/start.sh: -------------------------------------------------------------------------------- 1 | node app.js 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nagiostv", 3 | "version": "0.9.4", 4 | "type": "module", 5 | "private": true, 6 | "homepage": "./", 7 | "devDependencies": { 8 | "@testing-library/jest-dom": "^6.6.3", 9 | "@testing-library/react": "^15.0.7", 10 | "@types/jquery": "^3.5.32", 11 | "@types/js-cookie": "^3.0.6", 12 | "@types/lodash": "^4.17.17", 13 | "@types/luxon": "^3.6.2", 14 | "@types/node": "^18.19.103", 15 | "@types/react": "^18.3.22", 16 | "@types/react-dom": "^18.3.7", 17 | "@vitejs/plugin-react": "^4.5.0", 18 | "c8": "^9.1.0", 19 | "jsdom": "^24.1.3", 20 | "typescript": "^5.8.3", 21 | "vite": "^6.3.5", 22 | "vite-plugin-pwa": "^0.21.2", 23 | "vite-plugin-svgr": "^4.3.0", 24 | "vite-tsconfig-paths": "^5.1.4", 25 | "vitest": "^3.1.4" 26 | }, 27 | "dependencies": { 28 | "@fortawesome/fontawesome-svg-core": "^6.7.2", 29 | "@fortawesome/free-solid-svg-icons": "^6.7.2", 30 | "@fortawesome/react-fontawesome": "^0.2.2", 31 | "allotment": "^1.20.3", 32 | "axios": "^1.9.0", 33 | "clipboard-polyfill": "^3.0.3", 34 | "highcharts": "^12.2.0", 35 | "highcharts-react-official": "^3.2.2", 36 | "html2canvas": "^1.4.1", 37 | "jotai": "^2.12.4", 38 | "js-cookie": "^3.0.5", 39 | "lodash": "^4.17.21", 40 | "luxon": "^3.6.1", 41 | "motion": "^11.18.2", 42 | "react": "^18.3.1", 43 | "react-app-polyfill": "^3.0.0", 44 | "react-dom": "^18.3.1", 45 | "react-router-dom": "^6.30.1", 46 | "react-tooltip": "^4.5.1", 47 | "styled-components": "^5.3.11", 48 | "url-search-params-polyfill": "^8.2.5" 49 | }, 50 | "scripts": { 51 | "start": "vite", 52 | "build": "vite build", 53 | "test": "vitest watch", 54 | "test:no-watch": "vitest run", 55 | "test:coverage": "vitest run --coverage", 56 | "proxy": "node proxy.js" 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "ie 11", 66 | "last 1 chrome version", 67 | "last 1 firefox version", 68 | "last 1 safari version" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /proxy.js: -------------------------------------------------------------------------------- 1 | /************** 2 | * This is a slightly modified version of its source: 3 | * https://gist.github.com/mizzy/1342667/f861258311df61b0691ebf9c3d59e1a0030d32de#gistcomment-2870324 4 | */ 5 | const http = require('http'); 6 | const url = require('url'); 7 | 8 | /********** 9 | * Set these to connect to your Nagios server 10 | */ 11 | const NAGIOS_HOST = '192.168.1.100'; 12 | // Leave USER and PASS blank if you'd disabled Auth on the Nagios Server 13 | const NAGIOS_USER = 'nagiosadmin'; 14 | const NAGIOS_PASS = 'mypassword'; 15 | 16 | /********** 17 | * Use these to control the local proxy 18 | */ 19 | const PROXY_PORT = 8080; 20 | const DEBUG = false; 21 | 22 | 23 | /********** 24 | * Create the proxy and run it. 25 | */ 26 | const proxy = http.createServer((req, res) => { 27 | 28 | const request = url.parse(req.url); 29 | 30 | const options = { 31 | host: request.hostname, 32 | port: request.port || 80, 33 | path: request.path, 34 | method: req.method, 35 | headers: req.headers, 36 | }; 37 | options.host = NAGIOS_HOST; 38 | if (NAGIOS_USER && NAGIOS_PASS) { 39 | const auth = 'Basic ' + Buffer.from(NAGIOS_USER + ':' + NAGIOS_PASS).toString('base64'); 40 | options.headers['Authorization'] = auth; 41 | } 42 | 43 | console.log(`${options.method} http://${options.host}${options.path}`); 44 | if (DEBUG) console.log(options.headers); 45 | 46 | const backend_req = http.request(options, (backend_res) => { 47 | // CORS don't care header 48 | backend_res.headers['Access-Control-Allow-Origin'] = '*'; 49 | if (DEBUG) console.log(backend_res.headers) 50 | 51 | console.log('RESP', backend_res.statusCode, backend_res.statusMessage, backend_res.url) 52 | res.writeHead(backend_res.statusCode, backend_res.headers); 53 | 54 | 55 | 56 | backend_res.on('data', (chunk) => { 57 | res.write(chunk); 58 | }); 59 | 60 | backend_res.on('end', () => { 61 | res.end(); 62 | }); 63 | }); 64 | 65 | backend_req.on('error', err => console.error(`ERROR PROXYING REQUEST TO ${options.host}\n`, err)); 66 | 67 | req.on('error', err => { 68 | console.error('ERROR:', err) 69 | }); 70 | 71 | req.on('data', (chunk) => { 72 | backend_req.write(chunk); 73 | }); 74 | 75 | req.on('end', () => { 76 | backend_req.end(); 77 | }); 78 | 79 | 80 | }); 81 | proxy.on('listening', ()=>{ 82 | console.log(`[Nagios Proxy Server] 83 | Listening at : http://localhost:${PROXY_PORT}/ 84 | username : ${NAGIOS_USER} 85 | `); 86 | }) 87 | proxy.listen(PROXY_PORT); 88 | 89 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/auto-version-switch.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | /** 22 | * Functions 23 | */ 24 | function deleteFilesAndDirectories($dir) { 25 | $files = array_diff(scandir($dir), array('.','..')); 26 | foreach ($files as $file) { 27 | (is_dir("$dir/$file")) ? deleteFilesAndDirectories("$dir/$file") : unlink("$dir/$file"); 28 | } 29 | return rmdir($dir); 30 | } 31 | 32 | /** 33 | * Main 34 | */ 35 | $temp_dir = 'temp'; 36 | $cwd = getcwd(); 37 | 38 | $whoami = exec('whoami'); 39 | 40 | //echo "cwd is $cwd\n"; 41 | 42 | // test if php is installed, report back to browser 43 | if (isset($_GET['testphp']) && $_GET['testphp'] == 'true') { 44 | 45 | // get the path name from the script filename with dirname() 46 | $data = [ 'name' => 'NagiosTV', 'script' => dirname($_SERVER['SCRIPT_FILENAME']), 'whoami' => $whoami ]; 47 | header('Content-Type: application/json'); 48 | if (isset($_SERVER['HTTP_ORIGIN'])) { 49 | header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); 50 | } 51 | echo json_encode($data); 52 | 53 | } elseif (isset($_GET['version']) && $_GET['version']) { 54 | 55 | // capture requested version 56 | $version = $_GET['version']; 57 | $version_without_v = $version; 58 | $pos = strpos($version, "v"); 59 | //echo "pos found at [$pos]"; 60 | if ($pos !== false) { 61 | $version_without_v = substr($version, $pos + 1); 62 | } 63 | 64 | // download software from github 65 | // https://api.github.com/repos/chriscareycode/nagiostv-react/tags 66 | // https://api.github.com/repos/chriscareycode/nagiostv-react/releases 67 | 68 | $url = "https://github.com/chriscareycode/nagiostv-react/releases/download/v$version_without_v/nagiostv-$version_without_v.tar.gz"; 69 | 70 | // Use basename() function to return the base name of file 71 | $file_name = basename($url); 72 | 73 | // make temp directory if it does not exist 74 | if (!file_exists($temp_dir)) { 75 | $mkdir_success = mkdir($temp_dir, 0777, true); 76 | if ($mkdir_success) { 77 | echo "Temp directory $temp_dir created.\n"; 78 | } else { 79 | echo "Failed creating temp directory.\n"; 80 | exit(); 81 | } 82 | } else { 83 | // temp dir exists. delete all files in there 84 | echo "Temp directory exists, deleting files in there..\n"; 85 | deleteFilesAndDirectories($temp_dir); 86 | } 87 | 88 | // Use file_get_contents() function to get the file 89 | // from url and use file_put_contents() function to 90 | // save the file by using base name 91 | echo "Downloading $url to $temp_dir/\n"; 92 | 93 | if(file_put_contents("$temp_dir/$file_name", file_get_contents($url))) { 94 | echo "File $temp_dir/$file_name downloaded successfully.\n"; 95 | } 96 | else { 97 | echo "File $temp_dir/$file_name downloaded failed.\n"; 98 | exit(); 99 | } 100 | 101 | // extract the file 102 | 103 | shell_exec("tar xvfz $temp_dir/$file_name --directory $temp_dir/"); 104 | echo "Done extracting. Copying files from temp directory over top of the old build..\n"; 105 | echo "cp -r $cwd/$temp_dir/nagiostv/* $cwd/\n"; 106 | 107 | // 108 | /** 109 | * TODO: clean up the old files - this is risky since if something 110 | * up above did not work cleanly, then we could blow out their only good files before copy 111 | * 112 | * static/css/ 113 | * static/js 114 | * 115 | */ 116 | 117 | // copy the files over top ofo the old version 118 | shell_exec("cp -r $cwd/$temp_dir/nagiostv/* $cwd/"); 119 | echo "Done copying.\n"; 120 | 121 | echo "All done!\n"; 122 | echo "\n"; 123 | echo "REFRESH THE PAGE NOW to load the new code.\n"; 124 | 125 | 126 | } else { 127 | $data = [ 'name' => 'NagiosTV' ]; 128 | header('Content-Type: application/json'); 129 | echo json_encode($data); 130 | } 131 | 132 | 133 | 134 | ?> -------------------------------------------------------------------------------- /public/autoupdate.sh: -------------------------------------------------------------------------------- 1 | #/bin/bash 2 | 3 | # 4 | # NagiosTV https://nagiostv.com 5 | # Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 6 | # 7 | # This program is free software: you can redistribute it and/or modify 8 | # it under the terms of the GNU General Public License as published by 9 | # the Free Software Foundation, either version 2 of the License, or 10 | # (at your option) any later version. 11 | # 12 | # This program is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU General Public License for more details. 16 | # 17 | # You should have received a copy of the GNU General Public License 18 | # along with this program. If not, see . 19 | # 20 | 21 | 22 | # sudo chown -R pi . 23 | # sh autoupdate.sh 0.3.5 24 | # sudo chown -R nagios . 25 | 26 | show_permission_error () { 27 | # permission error we will share 28 | echo "It is possible you have a permissions issue." 29 | echo "Did you first change ownership of the folder to the logged in user?" 30 | echo "" 31 | echo "$ sudo chown -R $USER ." 32 | echo "" 33 | } 34 | 35 | # try to detect /usr/local/nagios/ owner, fall back to "nagios" 36 | NAGIOSUSER=$(stat -c '%U' ..) 37 | if [ ! $? -eq 0 ]; then 38 | NAGIOSUSER="nagios" 39 | fi 40 | 41 | echo "====================================================================" 42 | echo "NagiosTV autoupdate script" 43 | echo "This script will allow you to downgrade or upgrade to any release" 44 | echo "on github at https://github.com/chriscareycode/nagiostv-react/releases" 45 | echo "====================================================================" 46 | 47 | # test if parameter was provided 48 | if [ -z "$1" ] || [ "$1" = "help" ] || [ "$1" = "HELP" ] 49 | then 50 | echo "" 51 | echo "For this script to succeed, it needs permission to overwrite the nagiostv files and folders." 52 | echo "You will need to change the owner of the nagiostv folder to the currently logged in user with this command:" 53 | echo "" 54 | echo "$ sudo chown -R $USER ." 55 | echo "" 56 | echo "Then proceed with the autoupdate command:" 57 | echo "" 58 | echo "$ sh autoupdate.sh 0.3.6" 59 | echo "" 60 | echo "You can optionally change the folder owner back to the nagios user after:" 61 | echo "$ sudo chown -R $NAGIOSUSER ." 62 | echo "(if permissions are correct it probably does not matter)" 63 | echo "" 64 | exit 1 65 | fi 66 | 67 | 68 | 69 | # catch parameter of which version to update to 70 | VERSION=$1 71 | echo "Starting autoupdate to version $VERSION" 72 | 73 | # create upgrade folder if it does not exist 74 | [ -d autoupdate ] || mkdir autoupdate 75 | 76 | # empty the autoupdate folder 77 | [ autoupdate ] || rm -rf autoupdate/* 78 | 79 | # download the file from github with curl 80 | GITHUBPATH="https://github.com/chriscareycode/nagiostv-react/releases/download/v$VERSION/nagiostv-$VERSION.tar.gz" 81 | FILENAME="nagiostv-$VERSION.tar.gz" 82 | echo $FILE 83 | echo "Changing into autoupdate/ folder" 84 | cd autoupdate 85 | echo "Downloading $GITHUBPATH" 86 | curl -O -L --silent $GITHUBPATH > $FILENAME 87 | # check the return code from curl 88 | if [ ! $? -eq 0 ]; then 89 | echo "ERROR: Problem downloading file with curl. Aborting." 90 | show_permission_error 91 | cd .. 92 | exit 1 93 | fi 94 | 95 | #ls -la $FILENAME 96 | 97 | # verify the file exists 98 | if [ ! -f $FILENAME ]; then 99 | echo "" 100 | echo "ERROR: Downloaded file not found! Aborting." 101 | show_permission_error 102 | cd .. 103 | exit 1 104 | fi 105 | 106 | # untar the file 107 | echo "Extracting archive..." 108 | tar xfz $FILENAME 109 | # check the return code from tar 110 | if [ ! $? -eq 0 ]; then 111 | echo "" 112 | echo "ERROR: Problem extracting file. Aborting." 113 | echo "It is possible that you specified a version that does not exist." 114 | cd .. 115 | exit 1 116 | fi 117 | 118 | # copy the resulting files from nagiostv/autoupdate/nagiostv/* over the current version 119 | echo "Overwriting files..." 120 | cp -r nagiostv/* ../ 121 | # check if the copy succeeded 122 | if [ ! $? -eq 0 ]; then 123 | echo "" 124 | echo "ERROR: Problem copying files. Aborting." 125 | show_permission_error 126 | cd .. 127 | exit 1 128 | fi 129 | 130 | # change dir from nagiostv/autoupdate/ back to nagiostv/ 131 | cd .. 132 | 133 | # create client-settings if it does not exist, and set permission 134 | if [ ! -f client-settings.json ]; then 135 | touch client-settings.json 136 | fi 137 | chmod 777 client-settings.json 138 | 139 | # return success or failure 140 | echo "Update to v$VERSION complete. Refresh NagiosTV in the browser." 141 | exit 0 142 | -------------------------------------------------------------------------------- /public/connectors/livestatus-settings.ini.sample: -------------------------------------------------------------------------------- 1 | [livestatus] 2 | socket_path = /usr/local/nagios/var/rw/live.sock 3 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/favicon.png -------------------------------------------------------------------------------- /public/icon-1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/icon-1024.png -------------------------------------------------------------------------------- /public/icon-180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/icon-180.png -------------------------------------------------------------------------------- /public/icon-256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/icon-256.png -------------------------------------------------------------------------------- /public/js/polyfill.min.js: -------------------------------------------------------------------------------- 1 | /* Disable minification (remove `.min` from URL path) for more info */ 2 | 3 | (function(undefined) {}).call('object' === typeof window && window || 'object' === typeof self && self || 'object' === typeof global && global || {}); -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "NagiosTV", 3 | "name": "NagiosTV", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "icon-256.png", 12 | "sizes": "256x256", 13 | "type": "image/png" 14 | } 15 | ], 16 | "start_url": "./index.html", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff", 20 | "display": "fullscreen" 21 | } 22 | -------------------------------------------------------------------------------- /public/multi-nagios-server-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4-in-1 Nagios 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /public/sample-audio/critical.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/sample-audio/critical.mp3 -------------------------------------------------------------------------------- /public/sample-audio/ok.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/sample-audio/ok.mp3 -------------------------------------------------------------------------------- /public/sample-audio/warning.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/sample-audio/warning.mp3 -------------------------------------------------------------------------------- /public/sample-data/commentlist.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 0, 3 | "result": { 4 | "query_time": 1717133958000, 5 | "cgi": "statusjson.cgi", 6 | "user": "chris", 7 | "query": "commentlist", 8 | "query_status": "released", 9 | "program_start": 1716746118000, 10 | "last_data_update": 1717133958000, 11 | "type_code": 0, 12 | "type_text": "Success", 13 | "message": "" 14 | }, 15 | "data": { 16 | "selectors": { 17 | }, 18 | "commentlist": { 19 | "770": { 20 | "comment_id": 770, 21 | "comment_type": 2, 22 | "entry_type": 3, 23 | "source": 0, 24 | "persistent": false, 25 | "entry_time": 1717133941000, 26 | "expires": false, 27 | "expire_time": 0, 28 | "host_name": "camera-front", 29 | "service_description": "", 30 | "author": "Chris", 31 | "comment_data": "Testing comments." 32 | }, 33 | "771": { 34 | "comment_id": 771, 35 | "comment_type": 2, 36 | "entry_type": 3, 37 | "source": 0, 38 | "persistent": false, 39 | "entry_time": 1717133941000, 40 | "expires": false, 41 | "expire_time": 0, 42 | "host_name": "camera-back-gate", 43 | "service_description": "", 44 | "author": "Chris", 45 | "comment_data": "Testing comments 2." 46 | }, 47 | "772": { 48 | "comment_id": 772, 49 | "comment_type": 2, 50 | "entry_type": 3, 51 | "source": 0, 52 | "persistent": false, 53 | "entry_time": 1717133941000, 54 | "expires": false, 55 | "expire_time": 0, 56 | "host_name": "camera-front-car", 57 | "service_description": "Check Something", 58 | "author": "(Nagios Process)", 59 | "comment_data": "Notifications for this service are being suppressed because it was detected as having been flapping between different states (24.2% change >= 20.0% threshold). When the service state stabilizes and the flapping stops, notifications will be re-enabled." 60 | } 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /public/sample-data/hostcount.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 0, 3 | "result": { 4 | "query_time": 1713631611000, 5 | "cgi": "statusjson.cgi", 6 | "user": "chris", 7 | "query": "hostcount", 8 | "query_status": "released", 9 | "program_start": 1713052066000, 10 | "last_data_update": 1713631606000, 11 | "type_code": 0, 12 | "type_text": "Success", 13 | "message": "" 14 | }, 15 | "data": { 16 | "selectors": { 17 | }, 18 | "count": { 19 | "up": 38, 20 | "down": 0, 21 | "unreachable": 0, 22 | "pending": 0 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /public/sample-data/programstatus.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 0, 3 | "result": { 4 | "query_time": 1713715097000, 5 | "cgi": "statusjson.cgi", 6 | "user": "chris", 7 | "query": "programstatus", 8 | "query_status": "released", 9 | "program_start": 1713052066000, 10 | "last_data_update": 1713715096000, 11 | "type_code": 0, 12 | "type_text": "Success", 13 | "message": "" 14 | }, 15 | "data": { 16 | "programstatus": { 17 | "version": "4.4.6", 18 | "nagios_pid": 12607, 19 | "daemon_mode": true, 20 | "program_start": 1713052066000, 21 | "last_log_rotation": 1713682799000, 22 | "enable_notifications": true, 23 | "execute_service_checks": true, 24 | "accept_passive_service_checks": true, 25 | "execute_host_checks": true, 26 | "accept_passive_host_checks": true, 27 | "enable_event_handlers": true, 28 | "obsess_over_services": false, 29 | "obsess_over_hosts": false, 30 | "check_service_freshness": true, 31 | "check_host_freshness": true, 32 | "enable_flap_detection": true, 33 | "process_performance_data": false 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/sample-data/servicecount.json: -------------------------------------------------------------------------------- 1 | { 2 | "format_version": 0, 3 | "result": { 4 | "query_time": 1713631611000, 5 | "cgi": "statusjson.cgi", 6 | "user": "chris", 7 | "query": "servicecount", 8 | "query_status": "released", 9 | "program_start": 1713052066000, 10 | "last_data_update": 1713631606000, 11 | "type_code": 0, 12 | "type_text": "Success", 13 | "message": "" 14 | }, 15 | "data": { 16 | "selectors": { 17 | }, 18 | "count": { 19 | "ok": 136, 20 | "warning": 1, 21 | "critical": 0, 22 | "unknown": 0, 23 | "pending": 0 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /public/sample-image/nagios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/sample-image/nagios.png -------------------------------------------------------------------------------- /public/sample-image/resedit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/public/sample-image/resedit.png -------------------------------------------------------------------------------- /public/save-client-settings.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | // capture JSON POST 22 | $json = ''; 23 | if ($_SERVER['REQUEST_METHOD'] == 'POST') { 24 | $json = file_get_contents("php://input"); 25 | } 26 | 27 | // write JSON to file 28 | $myfile = fopen("client-settings.json", "w") or die("Unable to open file!"); 29 | fwrite($myfile, $json); 30 | fclose($myfile); 31 | 32 | header('Content-Type: application/json'); 33 | //echo json_encode($json); 34 | echo $json; 35 | 36 | ?> -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | .App {} 20 | 21 | .App-logo { 22 | animation: App-logo-spin infinite 20s linear; 23 | height: 80px; 24 | } 25 | 26 | .App-header { 27 | height: 150px; 28 | padding: 20px; 29 | color: white; 30 | } 31 | 32 | .App-title { 33 | font-size: 1.5em; 34 | } 35 | 36 | .App-intro { 37 | font-size: large; 38 | } 39 | 40 | @keyframes App-logo-spin { 41 | from { 42 | transform: rotate(0deg); 43 | } 44 | 45 | to { 46 | transform: rotate(360deg); 47 | } 48 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { screen, render } from "@testing-library/react"; 2 | import App from "./App"; 3 | 4 | describe("App tests", () => { 5 | it("should render the title", () => { 6 | render(); 7 | 8 | // expect( 9 | // screen.getByRole("heading", { 10 | // level: 1, 11 | // }) 12 | // ).toHaveTextContent("Vite + React"); 13 | 14 | // Write a test to make sure the div with id="Base" is in the DOM 15 | expect(screen.getByTestId("Base")).toBeInTheDocument(); // Add toBeInTheDocument to the expect statement 16 | 17 | }); 18 | }); -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import './App.css'; 20 | import Base from './components/Base'; 21 | import { Provider } from 'jotai' 22 | import { AppContextProvider } from "./components/AppContext"; 23 | 24 | const App = () => { 25 | 26 | return ( 27 |
28 | 29 | 30 | 31 | 32 | 33 |
34 | ); 35 | 36 | }; 37 | 38 | export default App; 39 | -------------------------------------------------------------------------------- /src/atoms/alertAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { AlertWrap } from 'types/hostAndServiceTypes'; 3 | 4 | export const alertIsFetchingAtom = atom(false); 5 | 6 | const initialState: AlertWrap = { 7 | error: false, 8 | errorCount: 0, 9 | errorMessage: '', 10 | lastUpdate: 0, 11 | response: {}, 12 | responseArray: [] 13 | }; 14 | 15 | export const alertAtom = atom(initialState); 16 | 17 | export const alertHowManyAtom = atom({ 18 | howManyAlerts: 0, 19 | howManyAlertSoft: 0, 20 | }); -------------------------------------------------------------------------------- /src/atoms/commentlistAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { CommentListResponseObject } from 'types/commentTypes'; 3 | 4 | interface CommentListAtom { 5 | error: boolean; 6 | errorCount: number; 7 | errorMessage: string; 8 | lastUpdate: number; 9 | response: Record; 10 | commentlistObject: { 11 | hosts: Record; 12 | services: Record; 13 | }; 14 | } 15 | 16 | const initialState: CommentListAtom = { 17 | error: false, 18 | errorCount: 0, 19 | errorMessage: '', 20 | lastUpdate: 0, 21 | response: {}, 22 | commentlistObject: { 23 | hosts: {}, 24 | services: {} 25 | }, 26 | }; 27 | 28 | export const commentlistAtom = atom(initialState); 29 | 30 | -------------------------------------------------------------------------------- /src/atoms/hostAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { HostWrap } from '../types/hostAndServiceTypes'; 3 | 4 | export const hostIsFetchingAtom = atom(false); 5 | 6 | export const hostIsFakeDataSetAtom = atom(false); 7 | 8 | const initialState: HostWrap = { 9 | error: false, 10 | errorCount: 0, 11 | errorMessage: '', 12 | lastUpdate: 0, 13 | response: {}, 14 | problemsArray: [] 15 | }; 16 | 17 | export const hostAtom = atom(initialState); 18 | 19 | export const hostHowManyAtom = atom({ 20 | howManyHosts: 0, 21 | howManyHostUp: 0, 22 | howManyHostDown: 0, 23 | howManyHostUnreachable: 0, 24 | howManyHostPending: 0, 25 | howManyHostAcked: 0, 26 | howManyHostScheduled: 0, 27 | howManyHostFlapping: 0, 28 | howManyHostSoft: 0, 29 | howManyHostNotificationsDisabled: 0, 30 | }); -------------------------------------------------------------------------------- /src/atoms/hostgroupAtom.js: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | const initialState = { 4 | error: false, 5 | errorCount: 0, 6 | errorMessage: '', 7 | lastUpdate: 0, 8 | response: {} 9 | }; 10 | 11 | export const hostgroupAtom = atom(initialState); 12 | 13 | export const servicegroupAtom = atom(initialState); -------------------------------------------------------------------------------- /src/atoms/programAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | export const programStatusIsFetchingAtom = atom(false); 4 | 5 | interface ProgramStatus { 6 | program_start: number; 7 | version: string; 8 | } 9 | 10 | export interface ProgramStatusWrap { 11 | error: boolean; 12 | errorCount: number; 13 | errorMessage: string; 14 | lastUpdate: number; 15 | response: ProgramStatus | null; 16 | } 17 | 18 | const initialState: ProgramStatusWrap = { 19 | error: false, 20 | errorCount: 0, 21 | errorMessage: '', 22 | lastUpdate: 0, 23 | response: null 24 | }; 25 | 26 | export const programStatusAtom = atom(initialState); 27 | -------------------------------------------------------------------------------- /src/atoms/serviceAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | import { ServiceWrap } from 'types/hostAndServiceTypes'; 3 | 4 | export const serviceIsFetchingAtom = atom(false); 5 | 6 | export const serviceIsFakeDataSetAtom = atom(false); 7 | 8 | const initialState: ServiceWrap = { 9 | error: false, 10 | errorCount: 0, 11 | errorMessage: '', 12 | lastUpdate: 0, 13 | response: {}, 14 | problemsArray: [] 15 | }; 16 | 17 | export const serviceAtom = atom(initialState); 18 | 19 | export const serviceHowManyAtom = atom({ 20 | howManyServices: 0, 21 | howManyServiceOk: 0, 22 | howManyServiceWarning: 0, 23 | howManyServiceUnknown: 0, 24 | howManyServiceCritical: 0, 25 | howManyServicePending: 0, 26 | howManyServiceAcked: 0, 27 | howManyServiceScheduled: 0, 28 | howManyServiceFlapping: 0, 29 | howManyServiceSoft: 0, 30 | howManyServiceNotificationsDisabled: 0, 31 | }); 32 | -------------------------------------------------------------------------------- /src/atoms/settingsState.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | // Types 3 | import { BigState, ClientSettings } from 'types/settings'; 4 | // Import Polyfills 5 | import 'url-search-params-polyfill'; 6 | 7 | // turn on demo mode if ?demo=true or we are hosting on nagiostv.com 8 | // demo mode uses fake data and rotates through a couple of alerts as an example 9 | const urlParams = new URLSearchParams(window.location.search); 10 | const isDemoMode = urlParams.get('demo') === 'true' || window.location.hostname === 'nagiostv.com'; 11 | if (isDemoMode) { 12 | console.log('Demo mode is on'); 13 | } 14 | 15 | const isStressTestMode = urlParams.get('stresstest') === 'true'; 16 | 17 | // turn on debug mode if ?debug=true 18 | const isDebugMode = urlParams.get('debug') === 'true'; 19 | 20 | // use fake data (for local development) if ?fakedata=true, or if demo mode is true (?demo=true) 21 | const useFakeSampleData = urlParams.get('fakedata') === 'true' || urlParams.get('fakeData') === 'true' || isDemoMode; 22 | 23 | //**************************************************************************** */ 24 | // state which is used internally by NagiosTV 25 | //**************************************************************************** */ 26 | 27 | const bigStateInitial: BigState = { 28 | 29 | currentVersion: 82, // This gets incremented with each new release (manually) 30 | currentVersionString: '0.9.4', // This gets incremented with each new release (manually) 31 | 32 | latestVersion: 0, 33 | latestVersionString: '', 34 | lastVersionCheckTime: 0, 35 | 36 | isDemoMode, 37 | isDebugMode, 38 | isStressTestMode, 39 | useFakeSampleData, 40 | 41 | isRemoteSettingsLoaded: false, 42 | isLocalSettingsLoaded: false, // I have this to render things only after local settings is loaded 43 | isDoneLoading: false, 44 | 45 | hideFilters: true, 46 | isLeftPanelOpen: false, 47 | }; 48 | 49 | //**************************************************************************** */ 50 | // state which is loaded and saved into client settings / server settings 51 | //**************************************************************************** */ 52 | export const clientSettingsInitial: ClientSettings = { 53 | 54 | titleString: 'NagiosTV', 55 | dataSource: 'cgi', 56 | baseUrl: '/nagios/cgi-bin/', // Base path to Nagios cgi-bin folder 57 | livestatusPath: 'connectors/livestatus.php', 58 | 59 | fetchHostFrequency: 30, // seconds 60 | fetchServiceFrequency: 30, // seconds 61 | fetchAlertFrequency: 60, // seconds 62 | fetchHostGroupFrequency: 3600, // seconds 63 | fetchCommentFrequency: 120, // seconds 64 | 65 | alertDaysBack: 30, 66 | alertHoursBack: 24, 67 | alertMaxItems: 5000, 68 | 69 | hostsAndServicesSideBySide: false, 70 | hideSummarySection: true, 71 | hideMostRecentAlertSection: true, 72 | hideServiceSection: false, 73 | hideServicePending: false, 74 | hideServiceWarning: false, 75 | hideServiceUnknown: false, 76 | hideServiceCritical: false, 77 | hideServiceAcked: false, 78 | hideServiceScheduled: false, 79 | hideServiceFlapping: false, 80 | hideServiceSoft: false, 81 | hideServiceNotificationsDisabled: false, 82 | serviceSortOrder: 'newest', 83 | 84 | hideHostSection: false, 85 | hideHostPending: false, 86 | hideHostDown: false, 87 | hideHostUnreachable: false, 88 | hideHostAcked: false, 89 | hideHostScheduled: false, 90 | hideHostFlapping: false, 91 | hideHostSoft: false, 92 | hideHostNotificationsDisabled: false, 93 | hostSortOrder: 'newest', 94 | 95 | hideHistory: false, 96 | hideHistoryTitle: false, 97 | hideHistory24hChart: false, 98 | hideHistoryChart: false, 99 | 100 | hideAlertSoft: false, 101 | 102 | hostgroupFilter: '', 103 | servicegroupFilter: '', 104 | 105 | versionCheckDays: 7, 106 | 107 | language: 'English', 108 | locale: 'en', 109 | dateFormat: 'fff', 110 | clockDateFormat: 'DD', 111 | clockTimeFormat: 'ttt', 112 | 113 | // audio and visual 114 | fontSizeEm: '1em', 115 | customLogoEnabled: false, 116 | customLogoUrl: './sample-image/resedit.png', 117 | doomguyEnabled: false, 118 | doomguyConcernedAt: 1, 119 | doomguyAngryAt: 2, 120 | doomguyBloodyAt: 4, 121 | showEmoji: false, 122 | speakItems: false, 123 | speakItemsVoice: '', 124 | playSoundEffects: false, 125 | soundEffectCritical: './sample-audio/critical.mp3', 126 | soundEffectWarning: './sample-audio/warning.mp3', 127 | soundEffectOk: './sample-audio/ok.mp3', 128 | showNextCheckInProgressBar: true, 129 | hideHamburgerMenu: false, 130 | hideBottomMenu: false, 131 | automaticScroll: false, 132 | automaticScrollTimeMultiplier: 4, 133 | automaticScrollWaitSeconds: 10, 134 | showMiniMap: false, 135 | miniMapWidth: 120, 136 | }; 137 | 138 | export const bigStateAtom = atom(bigStateInitial); 139 | 140 | export const clientSettingsAtom = atom(clientSettingsInitial); 141 | -------------------------------------------------------------------------------- /src/atoms/skipVersionAtom.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai'; 2 | 3 | export const skipVersionAtom = atom({ 4 | version: 0, 5 | version_string: '', 6 | }); 7 | -------------------------------------------------------------------------------- /src/components/AppContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useState } from "react"; 2 | 3 | export const AppContext = createContext({}); 4 | 5 | export function AppContextProvider({ children }) { 6 | const [preset, setPreset] = useState("moveToLeftFromRight"); 7 | const [enterAnimation, setEnterAnimation] = useState(""); 8 | const [exitAnimation, setExitAnimation] = useState(""); 9 | 10 | return ( 11 | 21 | {children} 22 | 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /src/components/Base.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | a { 20 | color: yellow; 21 | } 22 | 23 | .Base { 24 | position: fixed; 25 | inset: 0 0 0 0; 26 | } 27 | 28 | input[type=text] { 29 | width: 50%; 30 | } 31 | 32 | .margin-top-5 { 33 | margin-top: 5px; 34 | } 35 | 36 | .margin-top-10 { 37 | margin-top: 10px; 38 | } 39 | 40 | .margin-left-10 { 41 | margin-left: 10px; 42 | } 43 | 44 | .margin-right-10 { 45 | margin-right: 10px; 46 | } 47 | 48 | .softIcon { 49 | margin-right: 5px; 50 | } 51 | 52 | .display-none { 53 | display: none; 54 | } 55 | 56 | .settings {} 57 | 58 | .settings input { 59 | font-size: 1.1em; 60 | } 61 | 62 | .settings button { 63 | margin-left: 5px; 64 | font-size: 1.1em; 65 | } 66 | 67 | .display-inline-block { 68 | display: inline-block; 69 | } 70 | 71 | .SettingsButtonDiv button { 72 | background-color: #222; 73 | color: #999; 74 | border-color: #888; 75 | border-radius: 4px; 76 | } 77 | 78 | 79 | 80 | 81 | 82 | /* https://www.stephen.io/mediaqueries/ */ 83 | 84 | /* iPhone 10 */ 85 | @media only screen and (max-width: 1768px) {} 86 | 87 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2) { 88 | 89 | .BottomPanel svg { 90 | font-size: 1.7em !important; 91 | } 92 | } 93 | 94 | @media only screen and (max-device-width: 480px) {} 95 | 96 | @media only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3) {} 97 | 98 | .vertical-scroll { 99 | position: absolute; 100 | top: 0px; 101 | right: 0; 102 | bottom: 0; 103 | left: 0; 104 | overflow-y: scroll; 105 | padding: 0px 15px 20px 15px 106 | } 107 | 108 | .main-content { 109 | margin-left: 0px; 110 | transition: margin-left 250ms linear; 111 | position: absolute; 112 | top: 42px; 113 | right: 0; 114 | bottom: 0; 115 | left: 0; 116 | } 117 | 118 | /* also must update LeftPanel.css */ 119 | .main-content.left-panel-open { 120 | margin-left: 42px; 121 | } 122 | 123 | /* also must update RightPanel.css */ 124 | .main-content.right-panel-open { 125 | top: 0; 126 | } 127 | 128 | .SettingsButtonDiv { 129 | position: absolute; 130 | bottom: 3px; 131 | right: 3px; 132 | } 133 | 134 | .SettingsArea { 135 | border: 1px solid gray; 136 | background-color: #111; 137 | font-size: 1.0em; 138 | padding: 4px; 139 | border-radius: 5px; 140 | margin-bottom: 7px; 141 | } 142 | 143 | /* service summary (17 hosts (0 problems)) */ 144 | 145 | .service-summary { 146 | position: relative; 147 | margin-top: 13px; 148 | margin-bottom: 7px; 149 | clear: both; 150 | line-height: 1.5em; 151 | /*overflow: hidden;*/ 152 | 153 | } 154 | 155 | .service-summary-title { 156 | font-size: 1.0em; 157 | margin-right: 8px; 158 | margin-bottom: 5px; 159 | color: #bbb; 160 | } 161 | 162 | /* all ok */ 163 | 164 | .all-ok-item { 165 | position: relative; 166 | border: 1px solid #003400; 167 | border-left: 3px solid lime; 168 | background-color: #001d00; 169 | font-size: 1.5em; 170 | border-radius: 5px; 171 | overflow: hidden; 172 | } 173 | 174 | /* this creates the padding */ 175 | .all-ok-item>div { 176 | margin: 8px; 177 | margin-left: 10px; 178 | } 179 | 180 | /* these some-down-items live inside HostItems and ServiceItems */ 181 | /* probably should move to its own component */ 182 | .some-down-items { 183 | position: relative; 184 | border: 1px solid #004c00; 185 | border-left: 3px solid lime; 186 | background-color: #001d00; 187 | font-size: 1.5em; 188 | border-radius: 5px; 189 | overflow: hidden; 190 | max-height: 0px; 191 | opacity: 1; 192 | transition-delay: 0ms; 193 | transition: all 500ms ease-in; 194 | } 195 | 196 | /* this creates the padding */ 197 | .some-down-items>div { 198 | margin: 8px; 199 | margin-left: 10px; 200 | } 201 | 202 | .some-down-items.hidden { 203 | opacity: 0; 204 | } 205 | 206 | .some-down-items.visible { 207 | max-height: 120px; 208 | opacity: 1; 209 | } 210 | 211 | .some-down-hidden-text { 212 | font-size: 0.6em; 213 | } 214 | 215 | 216 | /* fun stuff */ 217 | 218 | .DoomguyWrapper { 219 | position: fixed; 220 | top: 10px; 221 | right: 18px; 222 | z-index: 11; 223 | } 224 | 225 | /* general purpose font related */ 226 | .uppercase { 227 | text-transform: uppercase; 228 | } 229 | 230 | .uppercase-first { 231 | text-transform: capitalize; 232 | } 233 | 234 | .display-inline-block { 235 | display: inline-block; 236 | } 237 | 238 | .font-size-0-6 { 239 | font-size: 0.6em; 240 | } 241 | 242 | .font-size-0-8 { 243 | font-size: 0.8em; 244 | } 245 | 246 | button { 247 | cursor: pointer; 248 | } 249 | 250 | .align-right { 251 | text-align: right; 252 | } 253 | 254 | .spacer-top { 255 | height: 55px; 256 | position: relative; 257 | } 258 | 259 | .settings-not-loaded { 260 | margin: 10px; 261 | } 262 | 263 | .plugin-output { 264 | font-size: 1.0em; 265 | } -------------------------------------------------------------------------------- /src/components/Dashboard.css: -------------------------------------------------------------------------------- 1 | .Dashboard {} 2 | 3 | .two-column-container { 4 | display: flex; 5 | flex-direction: row; 6 | margin-left: -10px; 7 | margin-right: -10px; 8 | } 9 | 10 | .two-column-column-1 { 11 | 12 | flex-shrink: 0; 13 | /* flex-basis: 50%; */ 14 | /* width: 50%; */ 15 | } 16 | 17 | .two-column-column-2 { 18 | 19 | flex-shrink: 0; 20 | /* flex-basis: 50%; */ 21 | /* width: 50%; */ 22 | } 23 | 24 | .two-column-box { 25 | /* width: 100%; */ 26 | border: 0px solid orange; 27 | position: relative; 28 | /* margin: 0px 10px; */ 29 | width: 50%; 30 | } 31 | 32 | .two-column-column-margin { 33 | margin: 0px 10px; 34 | } 35 | 36 | @media only screen and (max-width: 900px) { 37 | .two-column-container { 38 | flex-direction: column; 39 | margin-left: 0px; 40 | margin-right: 0px; 41 | } 42 | 43 | .two-column-box { 44 | margin: 0px 0px; 45 | width: 100%; 46 | } 47 | } -------------------------------------------------------------------------------- /src/components/Dashboard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // State Management 3 | import { useAtomValue } from 'jotai'; 4 | import { bigStateAtom, clientSettingsAtom } from '../atoms/settingsState'; 5 | // Dashboard Fetch (For HostGroup and Comment) 6 | import DashboardFetch from './DashboardFetch'; 7 | // Import Hosts and Services 8 | import HostGroupFilter from './hosts/HostGroupFilter'; 9 | import ServiceGroupFilter from './services/ServiceGroupFilter'; 10 | import Summary from './summary/Summary'; 11 | import HostSection from './hosts/HostSection'; 12 | import ServiceSection from './services/ServiceSection'; 13 | import AlertSection from './alerts/AlertSection'; 14 | // Demo mode 15 | import Demo from './Demo'; 16 | 17 | // CSS 18 | import './Dashboard.css'; 19 | import MostRecentAlert from './summary/MostRecentAlert'; 20 | 21 | const Dashboard = () => { 22 | 23 | const bigState = useAtomValue(bigStateAtom); 24 | const clientSettings = useAtomValue(clientSettingsAtom); 25 | 26 | // Chop the bigState into vars 27 | const { 28 | isDemoMode, 29 | isDoneLoading, 30 | hideFilters, 31 | } = bigState; 32 | 33 | // Chop the clientSettings into vars 34 | const { 35 | fontSizeEm, 36 | hideSummarySection, 37 | hideMostRecentAlertSection, 38 | hideHistory, 39 | hideHostSection, 40 | hideServiceSection, 41 | hostsAndServicesSideBySide, 42 | } = clientSettings; 43 | 44 | //console.log('Dashboard render()'); 45 | 46 | 47 | 48 | return ( 49 |
50 | 51 | {isDoneLoading &&
52 | 53 | 54 | 55 | {/* Hostgroup Filter Section */} 56 | {!hideFilters && } 57 | 58 | {/* Servicegroup Filter Section */} 59 | {!hideFilters && } 60 | 61 | {/* Summary Section */} 62 | {!hideSummarySection && } 63 | 64 | {/* Most Recent Alert Section */} 65 | {!hideMostRecentAlertSection && } 66 | 67 | {/* Hosts and Services Side by Side Enabled */} 68 | {hostsAndServicesSideBySide && ( 69 |
70 |
71 |
72 | {/* Hosts Section */} 73 | {!hideHostSection && } 74 |
75 |
76 |
77 |
78 | {/* Services Section */} 79 | {!hideServiceSection && } 80 |
81 |
82 |
83 | )} 84 | 85 | {/* Hosts and Services Side by Side Disabled (Default stacked) */} 86 | {!hostsAndServicesSideBySide && ( 87 |
88 | {/* Hosts Section */} 89 | {!hideHostSection && } 90 | 91 | {/* Services Section */} 92 | {!hideServiceSection && } 93 |
94 | )} 95 | 96 | {/* "AboveAlert" div (used for automatic scrolling routine) */} 97 |
98 | 99 | {/* Alert History Section */} 100 | {!hideHistory && } 101 | 102 | {/* Demo mode */} 103 | {isDemoMode && } 104 | 105 |
} 106 | 107 | {/* add space to the bottom of the dashboard */} 108 |
109 | 110 |
111 | 112 | ); 113 | }; 114 | 115 | // function isEqualMemo(prev, next) { 116 | // //console.log('Dashboard memoFn', prev, next); 117 | // return false; // update 118 | // } 119 | 120 | // We do not have 121 | export default React.memo(Dashboard); -------------------------------------------------------------------------------- /src/components/Demo.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | .Demo { 20 | border: 2px solid #949494; 21 | background-color: #353535; 22 | border-radius: 5px; 23 | padding: 5px; 24 | position: fixed; 25 | width: 500px; 26 | left: 50%; 27 | transform: translateX(-50%); 28 | bottom: 65px; 29 | z-index: 20; 30 | } 31 | 32 | .Demo .demo-header { 33 | background-color: #222; 34 | border-radius: 4px; 35 | padding: 1px 3px; 36 | text-align: center; 37 | } 38 | 39 | .Demo table { 40 | width: 100%; 41 | } 42 | 43 | .Demo table td { 44 | text-align: center; 45 | } 46 | 47 | .Demo button { 48 | width: 45%; 49 | font-size: 0.7em; 50 | margin: 0 2px; 51 | } 52 | 53 | .Demo .summary-label { 54 | font-size: 0.9em; 55 | } -------------------------------------------------------------------------------- /src/components/Doomguy/Doomguy.css: -------------------------------------------------------------------------------- 1 | .summary .doomguy-wrap { 2 | position: relative; 3 | top: 3px; 4 | right: 0px; 5 | width: 48px; 6 | height: 64px; 7 | } 8 | .TopPanel .doomguy-wrap { 9 | display: inline-block; 10 | height: 40px; 11 | width: 35px; 12 | position: relative; 13 | } 14 | 15 | .doomguy { 16 | position: absolute; 17 | top: 5px; 18 | right: 0px; 19 | height: 64px; 20 | width: 48px; 21 | /*transform: scale(1);*/ 22 | transform-origin: 100% 0%; 23 | cursor: pointer; 24 | } 25 | 26 | .doomguy1 { 27 | background-position: 0px 0px 28 | } 29 | 30 | .doomguy2 { 31 | background-position: 0px -64px 32 | } 33 | 34 | .doomguy3 { 35 | background-position: 0px -128px; 36 | } 37 | 38 | .doomguy4 { 39 | background-position: 0px -192px; 40 | } 41 | 42 | .doomguy5 { 43 | background-position: 0px -256px; 44 | } 45 | 46 | .doomguy6 { 47 | background-position: 0px -320px; 48 | } 49 | 50 | .doomguy7 { 51 | background-position: 0px -384px; 52 | } 53 | 54 | .doomguy8 { 55 | background-position: 0px -448px; 56 | } 57 | 58 | .doomguy9 { 59 | background-position: 0px -512px; 60 | } 61 | 62 | .doomguy10 { 63 | background-position: 0px -576px; 64 | } 65 | 66 | /* this one is an exception to the 64 rule */ 67 | .doomguy11 { 68 | background-position: 0px -640px 69 | } 70 | 71 | .doomguy12 { 72 | background-position: 0px -704px 73 | } 74 | 75 | .doomguy13 { 76 | background-position: 0px -768px; 77 | } 78 | 79 | .doomguy14 { 80 | background-position: 0px -832px; 81 | } 82 | 83 | .doomguy15 { 84 | background-position: 0px -896px; 85 | } 86 | 87 | .doomguy16 { 88 | background-position: 0px -960px; 89 | } 90 | 91 | .doomguy17 { 92 | background-position: 0px -1024px; 93 | } 94 | 95 | .doomguy18 { 96 | background-position: 0px -1088px; 97 | } 98 | 99 | .doomguy19 { 100 | background-position: 0px -1152px; 101 | } 102 | 103 | .doomguy20 { 104 | background-position: 0px -1216px; 105 | } 106 | 107 | .doomguy20 { 108 | background-position: 0px -1280px; 109 | } 110 | 111 | .doomguy21 { 112 | background-position: 0px -1344px; 113 | } 114 | 115 | .doomguy22 { 116 | background-position: 0px -1408px; 117 | } 118 | 119 | .doomguy23 { 120 | background-position: 0px -1472px; 121 | } 122 | 123 | .doomguy24 { 124 | background-position: 0px -1536px; 125 | } 126 | 127 | .doomguy25 { 128 | background-position: 0px -1600px; 129 | } 130 | 131 | .doomguy26 { 132 | background-position: 0px -1664px; 133 | } -------------------------------------------------------------------------------- /src/components/Doomguy/Doomguy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/src/components/Doomguy/Doomguy.png -------------------------------------------------------------------------------- /src/components/Doomguy/Doomguy.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { useState } from 'react'; 20 | 21 | // State Management 22 | import { useAtomValue } from 'jotai'; 23 | import { clientSettingsAtom } from '../../atoms/settingsState'; 24 | import { hostHowManyAtom } from '../../atoms/hostAtom'; 25 | import { serviceHowManyAtom } from '../../atoms/serviceAtom'; 26 | 27 | import './Doomguy.css'; 28 | // @ts-ignore-next-line 29 | import doomguyImage from './Doomguy.png'; 30 | /* 31 | Doomguy will be happy at 0 services down 32 | Doomguy will be angry at < 4 services down 33 | Doomguy will be bloody at >= 4 services down 34 | */ 35 | 36 | const Doomguy = ({ scaleCss, style }: { 37 | scaleCss?: string, 38 | style?: React.CSSProperties 39 | }) => { 40 | 41 | const smileClasses = ['doomguy20', 'doomguy21', 'doomguy22', 'doomguy23']; 42 | const happyClasses = ['doomguy1', 'doomguy6', 'doomguy11']; 43 | const angryClasses = ['doomguy2', 'doomguy3', 'doomguy7', 'doomguy8', 'doomguy12', 'doomguy13', 'doomguy16', 'doomguy17', 'doomguy18', 'doomguy19']; 44 | const bloodyClasses = ['doomguy4', 'doomguy5', 'doomguy9', 'doomguy10', 'doomguy14', 'doomguy15', 'doomguy24', 'doomguy25']; 45 | 46 | const clientSettings = useAtomValue(clientSettingsAtom); 47 | const hostHowManyState = useAtomValue(hostHowManyAtom); 48 | const serviceHowManyState = useAtomValue(serviceHowManyAtom); 49 | 50 | const [clicked, setClicked] = useState(false); // Clicking his face will temporarily make him angry 51 | 52 | const howManyDown = 53 | hostHowManyState.howManyHostDown + 54 | serviceHowManyState.howManyServiceWarning + 55 | serviceHowManyState.howManyServiceCritical; 56 | 57 | let doomguyClass = 'doomguy'; 58 | let classes: any[] = []; 59 | if (howManyDown === -1) { 60 | classes = smileClasses; 61 | } else if (howManyDown === 0) { 62 | classes = happyClasses; 63 | } else if (howManyDown >= clientSettings.doomguyAngryAt && howManyDown < clientSettings.doomguyBloodyAt) { 64 | classes = angryClasses; 65 | } else if (howManyDown >= clientSettings.doomguyBloodyAt) { 66 | classes = bloodyClasses; 67 | } else { 68 | classes = happyClasses; 69 | } 70 | 71 | // Get a random item from the list 72 | let item = classes[Math.floor(Math.random() * classes.length)]; 73 | 74 | // If clicked then force smile 75 | if (clicked) { 76 | item = 'doomguy23'; 77 | } 78 | 79 | // Get the class name and scale 80 | doomguyClass = 'doomguy ' + item; 81 | //const transformCss = 'scale(' + clientSettings.doomguyCssScale + ')'; 82 | const transformCss = { 83 | backgroundImage: 'url(' + doomguyImage + ')', 84 | transform: `scale(${scaleCss ? scaleCss : '1'})`, 85 | }; 86 | 87 | //console.log('doomguyClass is ' + doomguyClass + ' ' + new Date()); 88 | 89 | // If clicked then force smile 90 | const clickedDoomguy = () => { 91 | setClicked(true); 92 | // TODO this could trigger a setState on unmounted component 93 | setTimeout(() => { 94 | setClicked(false); 95 | }, 2000); 96 | }; 97 | 98 | return ( 99 |
100 |
101 |
102 |
103 | ); 104 | 105 | }; 106 | 107 | export default Doomguy; 108 | -------------------------------------------------------------------------------- /src/components/Help.css: -------------------------------------------------------------------------------- 1 | .Help a { 2 | color: #6fbbf3; 3 | } 4 | 5 | .help-option { 6 | cursor: pointer; 7 | margin-bottom: 5px; 8 | display: inline-block; 9 | } 10 | 11 | .help-option:hover { 12 | color: orange; 13 | } 14 | 15 | .help-bottom-area { 16 | position: fixed; 17 | bottom: 40px; 18 | left: 50%; 19 | transform: translateX(-50%); 20 | } -------------------------------------------------------------------------------- /src/components/Help.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // React Router 20 | import { Link } from "react-router-dom"; 21 | import './Help.css'; 22 | 23 | const Help = () => { 24 | 25 | return ( 26 |
27 |

NagiosTV Info and Help

28 | 29 |
30 | 31 |
32 | 33 |
34 |
NagiosTV website: https://nagiostv.com
35 |
36 | 37 |
38 |
NagiosTV by Chris Carey https://chriscarey.com
39 |
40 |
41 | ); 42 | 43 | } 44 | 45 | export default Help; 46 | -------------------------------------------------------------------------------- /src/components/Settings.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | .Settings { 20 | background-color: #111; 21 | position: relative; 22 | margin-bottom: 30px; 23 | } 24 | 25 | .Settings select { 26 | border: 1px solid #ccc; 27 | } 28 | 29 | /* .settings-top-space-for-header { 30 | height: 45px; 31 | } */ 32 | 33 | .settings-header { 34 | position: -webkit-sticky; 35 | position: sticky; 36 | top: 0px; 37 | left: 0px; 38 | right: 0px; 39 | background-color: #2f2f2f; 40 | border: 0px solid orange; 41 | z-index: 2; 42 | } 43 | 44 | .settings-header-heading { 45 | padding-top: 15px; 46 | padding-bottom: 10px; 47 | font-size: 1.1em; 48 | margin-left: 15px; 49 | color: #ddd; 50 | } 51 | 52 | .settings-header-buttons { 53 | position: absolute; 54 | top: 10px; 55 | right: 15px; 56 | bottom: 5px; 57 | font-size: 0.9em; 58 | } 59 | 60 | .settings-header-buttons button { 61 | font-size: 1.0em; 62 | } 63 | 64 | .SettingsCloseButton { 65 | background-color: #333; 66 | color: orange; 67 | border: 2px solid orange; 68 | border-radius: 5px; 69 | padding: 5px 10px; 70 | cursor: pointer; 71 | margin-left: 10px; 72 | } 73 | 74 | .SettingSaveMessage { 75 | display: inline-block; 76 | } 77 | 78 | .SettingsSaveButton { 79 | background-color: #333; 80 | color: lime; 81 | border: 2px solid green; 82 | border-radius: 5px; 83 | padding: 5px 10px; 84 | cursor: pointer; 85 | } 86 | 87 | .SettingsDeleteLocalSettingsButton { 88 | background-color: #333; 89 | color: orange; 90 | font-weight: bold; 91 | font-size: 0.8em !important; 92 | border: 2px solid orange; 93 | padding: 5px 10px; 94 | cursor: pointer; 95 | margin-top: 8px; 96 | } 97 | 98 | .SettingsSaveToServerButton { 99 | background-color: #333; 100 | color: lime; 101 | font-weight: bold; 102 | border: 2px solid green; 103 | padding: 5px 10px; 104 | cursor: pointer; 105 | margin-left: 10px; 106 | } 107 | 108 | .SettingsShowJsonButton { 109 | background-color: #333; 110 | color: orange; 111 | font-weight: bold; 112 | border: 2px solid orange; 113 | padding: 5px 10px; 114 | cursor: pointer; 115 | margin-left: 0px; 116 | } 117 | 118 | .SettingsSaveToServerButton:disabled { 119 | background-color: #333; 120 | color: lime; 121 | font-weight: bold; 122 | border: 2px solid green; 123 | padding: 5px 10px; 124 | cursor: default; 125 | margin-left: 10px; 126 | opacity: 0.6; 127 | } 128 | 129 | .SettingsSection { 130 | border: 1px solid #eee; 131 | border-radius: 5px; 132 | padding: 5px; 133 | margin-bottom: 10px; 134 | background-color: #444; 135 | } 136 | 137 | .SettingsSection button {} 138 | 139 | .raw-json-settings { 140 | border: 2px solid #424242; 141 | background-color: #333; 142 | padding: 10px; 143 | margin-top: 10px; 144 | margin-right: 20px; 145 | width: 100%; 146 | max-width: 80vw; 147 | min-height: 100px; 148 | word-wrap: break-word; 149 | white-space: pre-wrap; 150 | 151 | } 152 | 153 | .SettingsCenterDiv { 154 | position: absolute; 155 | left: 50%; 156 | top: 50%; 157 | transform: translate(-50%, -50%); 158 | } 159 | 160 | .SettingsTestButton { 161 | margin-left: 10px; 162 | } 163 | 164 | table.SettingsTable { 165 | border: 1px solid rgb(60, 60, 60); 166 | background-color: rgb(38, 38, 38); 167 | width: 100%; 168 | margin-bottom: 10px; 169 | margin-top: 15px; 170 | } 171 | 172 | table.SettingsTable td.SettingsTableHeader { 173 | background-color: #333; 174 | border-bottom: 2px solid #444; 175 | padding: 9px; 176 | margin-bottom: 5px; 177 | } 178 | 179 | table.SettingsTable th { 180 | text-align: right; 181 | padding: 8px; 182 | white-space: nowrap; 183 | vertical-align: top; 184 | } 185 | 186 | table.SettingsTable td { 187 | padding: 4px; 188 | width: 70%; 189 | } 190 | 191 | table.SettingsTable input[type=text] { 192 | font-size: 1.1em; 193 | width: 80%; 194 | } 195 | 196 | table.SettingsTable input[type=number] { 197 | font-size: 1.1em; 198 | width: 100px; 199 | } 200 | 201 | table.SettingsTable button { 202 | font-size: 1.1em; 203 | border-radius: 4px; 204 | } 205 | 206 | table.SettingsTable select { 207 | font-size: 1.1em; 208 | } 209 | 210 | .Settings .input-error { 211 | background-color: pink; 212 | } 213 | 214 | .Settings .settings-unsaved-changes-text { 215 | background-color: brown; 216 | color: yellow; 217 | padding: 4px 8px; 218 | border-radius: 4px; 219 | } -------------------------------------------------------------------------------- /src/components/SettingsFakeData.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * SettingsFakeData component 3 | * In this component, we will loop over all the fake data and massage the data for better UI dev experience 4 | * 5 | * Particularly I wanted to fix the issue when in fake data mode, all items say "Checking now..." as 6 | * they have a next_check date in the past. This is because our fake data set is a snapshot of a moment in the past. 7 | * Let's loop over all these items, and if the next_check 8 | * is in the past, set it to a random time in the future. 9 | */ 10 | 11 | import { hostAtom } from "atoms/hostAtom"; 12 | import { serviceAtom } from "atoms/serviceAtom" 13 | import { bigStateAtom, clientSettingsAtom } from "atoms/settingsState"; 14 | import { useAtom, useAtomValue, useSetAtom } from "jotai"; 15 | import { useEffect } from "react"; 16 | 17 | const startAfterSeconds = 4; // This is how long to wait before starting the first/initial fake data 18 | const fakeOutIntervalSeconds = 40; // This needs to be longer than the polling interval for hosts and services. 19 | 20 | const SettingsFakeData = () => { 21 | // State Management state (main) 22 | const [bigState, setBigState] = useAtom(bigStateAtom); 23 | // const clientSettings = useAtomValue(clientSettingsAtom); 24 | const setHostState = useSetAtom(hostAtom); 25 | const setServiceState = useSetAtom(serviceAtom); 26 | 27 | useEffect(() => { 28 | 29 | // If we are in demo mode, loop over all hosts and services and set next_check to a random time in the future 30 | const fakeOutTheData = () => { 31 | setHostState(prev => { 32 | // Loop over all hosts 33 | const newArr = [...prev.problemsArray]; 34 | newArr.forEach((host) => { 35 | if (host.next_check < Date.now()) { 36 | host.next_check = Date.now() + Math.floor(Math.random() * 500000); 37 | } 38 | }); 39 | return { ...prev, problemsArray: newArr }; 40 | }); 41 | setServiceState(prev => { 42 | // Loop over all services 43 | const newArr = [...prev.problemsArray]; 44 | newArr.forEach((service) => { 45 | if (service.next_check < Date.now()) { 46 | service.next_check = Date.now() + Math.floor(Math.random() * 500000); 47 | } 48 | }); 49 | return { ...prev, problemsArray: newArr }; 50 | }); 51 | } 52 | 53 | let interval: NodeJS.Timeout; 54 | if (bigState.useFakeSampleData) { 55 | // Run one after 5s 56 | setTimeout(() => { 57 | console.log('Running fakeOutTheData to fake out the fake data...'); 58 | fakeOutTheData(); 59 | }, startAfterSeconds * 1000); 60 | 61 | // Start an interval of 1 minute to run the fakeOutTheData function 62 | console.log('Starting an interval to call fakeOutTheData...'); 63 | interval = setInterval(() => { 64 | fakeOutTheData(); 65 | }, fakeOutIntervalSeconds * 1000); 66 | } 67 | 68 | // Cleanup 69 | return () => { 70 | if (interval) { 71 | clearInterval(interval); 72 | } 73 | } 74 | }, [ 75 | bigState.useFakeSampleData, 76 | setHostState, 77 | setServiceState, 78 | ]); 79 | 80 | return (<>); 81 | }; 82 | 83 | export default SettingsFakeData; 84 | -------------------------------------------------------------------------------- /src/components/Update.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | .Update textarea { 20 | width: 95%; 21 | height: 200px; 22 | background-color: #222; 23 | color: lime; 24 | border: 2px solid yellow; 25 | } 26 | 27 | .Update a { 28 | color: #6fbbf3; 29 | } 30 | 31 | .Update h3 { 32 | color: #adadad; 33 | } 34 | 35 | .auto-update-error { 36 | background-color: red; 37 | color: yellow; 38 | border-radius: 4px; 39 | display: inline-block; 40 | padding: 3px 8px; 41 | } 42 | 43 | .auto-update-chown-command { 44 | border: 1px solid #ccc; 45 | background-color: #444; 46 | display: inline-block; 47 | padding: 2px 6px; 48 | } 49 | 50 | .auto-update-button { 51 | color: #0f0; 52 | border: 2px solid green; 53 | background-color: #333; 54 | font-weight: 700; 55 | padding: 5px 10px; 56 | cursor: pointer; 57 | } 58 | 59 | .auto-update-button:disabled { 60 | opacity: 0.6 !important; 61 | cursor: default !important; 62 | } 63 | 64 | .update-help-message { 65 | border: 1px solid #464646; 66 | border-radius: 5px; 67 | background-color: #222; 68 | padding: 10px; 69 | } 70 | 71 | .update-server-setup-instructions { 72 | background-color: #003a00; 73 | margin-top: 20px; 74 | border: 1px solid lime; 75 | border-radius: 5px; 76 | padding: 15px; 77 | 78 | } -------------------------------------------------------------------------------- /src/components/alerts/AlertFilters.css: -------------------------------------------------------------------------------- 1 | .filter-ok-label { 2 | font-size: 0.7em; 3 | padding: 2px 4px; 4 | border-radius: 4px; 5 | position: relative; 6 | top: -2px; 7 | white-space: nowrap; 8 | } 9 | 10 | .filter-ok-label-green { 11 | border: 1px solid #004c00; 12 | background-color: #006700; 13 | color: lime; 14 | 15 | } 16 | 17 | .filter-ok-label-gray { 18 | border: 1px solid #545454; 19 | background-color: #3e3e3e; 20 | color: #ccc; 21 | } -------------------------------------------------------------------------------- /src/components/alerts/AlertFilters.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // State Management 20 | import { useAtom, useAtomValue } from 'jotai'; 21 | import { bigStateAtom, clientSettingsAtom } from '../../atoms/settingsState'; 22 | // Helpers 23 | import { translate } from '../../helpers/language'; 24 | import Checkbox from '../widgets/FilterCheckbox'; 25 | import { saveLocalStorage } from 'helpers/nagiostv'; 26 | // CSS 27 | import './AlertFilters.css'; 28 | 29 | interface AlertFiltersProps { 30 | howManyAlertSoft: number; 31 | } 32 | 33 | const AlertFilters = ({ 34 | howManyAlertSoft, 35 | }: AlertFiltersProps) => { 36 | 37 | const bigState = useAtomValue(bigStateAtom); 38 | const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom); 39 | 40 | // Chop the bigState into vars 41 | const { 42 | hideFilters, 43 | } = bigState; 44 | 45 | // Chop the clientSettings into vars 46 | const { 47 | hideAlertSoft, 48 | language, 49 | } = clientSettings; 50 | 51 | const handleCheckboxChange = (e: React.ChangeEvent, propName: string, dataType: 'checkbox') => { 52 | // This will get called twice (see note below). The little hack there deals with it 53 | // So we actually do not want e.preventDefault(); here 54 | //console.log('handleCheckboxChange', e); 55 | 56 | // we put this to solve the bubble issue where the click goes through the label then to the checkbox 57 | if (typeof e.target.checked === 'undefined') { return; } 58 | 59 | console.log('handleCheckboxChange going through'); 60 | 61 | let val: boolean | string = true; 62 | if (dataType === 'checkbox') { 63 | val = (!e.target.checked); 64 | } else { 65 | val = e.target.value; 66 | } 67 | 68 | // Save to state 69 | setClientSettings(settings => { 70 | saveLocalStorage('Alert Filters', { 71 | ...settings, 72 | [propName]: val, 73 | }); 74 | return ({ 75 | ...settings, 76 | [propName]: val, 77 | }); 78 | }); 79 | }; 80 | 81 | return ( 82 | <> 83 | {/*{howManyAlerts} Alerts*/} 84 | 85 | {(!hideFilters || howManyAlertSoft !== 0) && 86 |   87 | 96 | } 97 | 98 | ); 99 | 100 | }; 101 | 102 | export default AlertFilters; 103 | -------------------------------------------------------------------------------- /src/components/alerts/AlertItem.css: -------------------------------------------------------------------------------- 1 | .AlertItem { 2 | background-color: #222; 3 | border: 0px solid #9c9c9c; 4 | font-size: 1em; 5 | padding: 4px; 6 | border-radius: 5px; 7 | margin-bottom: 5px; 8 | 9 | overflow: hidden; 10 | color: white; 11 | justify-content: center; 12 | } 13 | 14 | .AlertItemRight { 15 | float: right; 16 | border: 0px solid #ccc; 17 | /*background-color: #444;*/ 18 | border-radius: 5px; 19 | padding: 0px 5px; 20 | position: relative; 21 | top: 0px; 22 | left: 4px; 23 | text-align: right; 24 | } 25 | 26 | .alert-item-right-date { 27 | font-size: 0.8em; 28 | color: #bbb; 29 | } 30 | 31 | /* soft */ 32 | .alert-item-state-type-2 { 33 | color: #bbb; 34 | font-size: 0.7em; 35 | } 36 | 37 | /* hard */ 38 | .alert-item-state-type-1 { 39 | font-size: 0.7em; 40 | } 41 | 42 | .alert-item-host-name { 43 | 44 | } 45 | 46 | .alert-item-description { 47 | font-size: 1em; 48 | padding: 2px 4px; 49 | border-radius: 4px; 50 | position: relative; 51 | top: 0px; 52 | border: 0px solid #545454; 53 | background-color: #101010; 54 | margin-left: 2px; 55 | margin-right: 5px; 56 | color: #fbc37c; 57 | } 58 | 59 | .alert-item-clickable { 60 | cursor: pointer; 61 | } 62 | 63 | .alert-item-clickable:hover { 64 | text-decoration: underline; 65 | } 66 | -------------------------------------------------------------------------------- /src/components/alerts/AlertItem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { translate } from '../../helpers/language'; 20 | import { formatDateTimeLocale } from '../../helpers/dates'; 21 | import { ifQuietFor } from '../../helpers/date-math'; 22 | import { alertTextClass, alertBorderClass } from '../../helpers/colors'; 23 | import { nagiosAlertState, nagiosAlertStateType } from '../../helpers/nagios'; 24 | import QuietFor from './QuietFor'; 25 | //import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 26 | //import { faAdjust } from '@fortawesome/free-solid-svg-icons'; 27 | 28 | // css 29 | import './AlertItem.css'; 30 | import { Alert } from 'types/hostAndServiceTypes'; 31 | import { ClientSettings } from 'types/settings'; 32 | 33 | interface AlertItemProps { 34 | isDemoMode: boolean; 35 | settings: ClientSettings; 36 | e: Alert; 37 | i: number; 38 | language: string; 39 | locale: string; 40 | dateFormat: string; 41 | prevtime: number; 42 | } 43 | 44 | const AlertItem = (props: AlertItemProps) => { 45 | 46 | const openNagiosHostPage = () => { 47 | const isDemoMode = props.isDemoMode; 48 | if (isDemoMode) { 49 | return; 50 | } 51 | 52 | const { e } = props; 53 | let hostName; 54 | if (e.object_type === 1) { 55 | hostName = e.name; 56 | } 57 | if (e.object_type === 2) { 58 | hostName = e.host_name; 59 | } 60 | const baseUrl = props.settings.baseUrl; 61 | const url = encodeURI(`${baseUrl}extinfo.cgi?type=1&host=${hostName}`); 62 | const win = window.open(url, '_blank'); 63 | win?.focus(); 64 | } 65 | 66 | const openNagiosServicePage = () => { 67 | const isDemoMode = props.isDemoMode; 68 | if (isDemoMode) { 69 | return; 70 | } 71 | 72 | const { e } = props; 73 | let hostName; 74 | if (e.object_type === 1) { 75 | hostName = e.name; 76 | } 77 | if (e.object_type === 2) { 78 | hostName = e.host_name; 79 | } 80 | const baseUrl = props.settings.baseUrl; 81 | const url = encodeURI(`${baseUrl}extinfo.cgi?type=2&host=${hostName}&service=${e.description}`); 82 | const win = window.open(url, '_blank'); 83 | win?.focus(); 84 | } 85 | 86 | const { language, locale, dateFormat } = props; 87 | const howMuchTimeIsQuietTime = 10; 88 | const { e, i } = props; 89 | const isSoft = e.state_type === 2; 90 | 91 | return ( 92 |
93 | {/* show quiet for */} 94 | {(i > 0) && ifQuietFor(e.timestamp, props.prevtime, howMuchTimeIsQuietTime) && 95 | 101 | } 102 | {/* show alert item */} 103 |
104 |
105 | {/*isSoft && */} 106 | {/* {1 === 2 && ({e.state_type})} */} 107 | {translate(nagiosAlertStateType(e.state_type), language)} 108 | {' '} 109 | {/* {1 === 2 && ({e.state})} */} 110 | {/* {1 === 2 && ({e.object_type})} */} 111 | {translate(nagiosAlertState(e.state), language)}{' '} 112 | 113 |
{formatDateTimeLocale(e.timestamp, locale, dateFormat)}
114 | 115 |
116 | 117 | 118 |
119 | {/* host */} 120 | {e.object_type === 1 && {e.name}} 121 | {/* service */} 122 | {e.object_type === 2 && {e.host_name}} 123 | {' '} 124 | 125 | {e.object_type === 2 && {e.description}} 126 | {e.plugin_output} 127 | 128 |
129 |
130 | 131 |
132 |
133 | ); 134 | 135 | } 136 | 137 | export default AlertItem; 138 | -------------------------------------------------------------------------------- /src/components/alerts/AlertItems.css: -------------------------------------------------------------------------------- 1 | .AlertItems { 2 | margin-top: 12px; 3 | opacity: 0.8; 4 | } 5 | 6 | .ShowMoreArea { 7 | margin-top: 20px; 8 | margin-bottom: 20px; 9 | text-align: center; 10 | min-height: 20px; 11 | } 12 | 13 | .ShowMoreArea button { 14 | font-size: 1.1em; 15 | border-radius: 5px; 16 | margin: 0 4px; 17 | color: white; 18 | background-color: #333; 19 | border: 0px solid lime; 20 | } -------------------------------------------------------------------------------- /src/components/alerts/AlertItems.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Component } from 'react'; 20 | import { translate } from '../../helpers/language'; 21 | 22 | import AlertItem from './AlertItem'; 23 | import QuietFor from './QuietFor'; 24 | 25 | import { Alert } from 'types/hostAndServiceTypes'; 26 | import { ClientSettings } from 'types/settings'; 27 | // CSS 28 | import '../animation.css'; 29 | import '../services/ServiceItems.css'; 30 | import './AlertItems.css'; 31 | 32 | interface AlertItemsProps { 33 | items: Alert[]; 34 | settings: ClientSettings; 35 | isDemoMode: boolean; 36 | } 37 | 38 | class AlertItems extends Component { 39 | 40 | constructor(props: AlertItemsProps) { 41 | super(props); 42 | 43 | this.showMore = this.showMore.bind(this); 44 | this.showLess = this.showLess.bind(this); 45 | } 46 | 47 | state = { 48 | howManyToRender: 100, // This value is updated when user clicks showMore or showLess. 49 | pageSize: 100 // This stays const 50 | }; 51 | 52 | showMore() { 53 | this.setState({ 54 | howManyToRender: this.state.howManyToRender + this.state.pageSize 55 | }); 56 | } 57 | 58 | showLess() { 59 | this.setState({ 60 | howManyToRender: this.state.howManyToRender - this.state.pageSize 61 | }); 62 | } 63 | 64 | render() { 65 | 66 | const filteredHistoryArray = this.props.items.filter(item => { 67 | if (this.props.settings.hideAlertSoft) { 68 | if (item.state_type === 2) { return false; } 69 | } 70 | return true; 71 | }); 72 | 73 | let trimmedItems = [...filteredHistoryArray]; 74 | trimmedItems.length = this.state.howManyToRender; 75 | const { language, locale, dateFormat } = this.props.settings; 76 | 77 | return ( 78 |
79 | {/* always show one quiet for (if we have at least 1 item) */} 80 | {this.props.items.length > 1 && 81 | 87 | } 88 | 89 | {/* loop through the trimmed items */} 90 | {trimmedItems.map((e, i) => { 91 | const host = (e.object_type === 1 ? e.name : e.host_name); 92 | const prevtime = (i > 0 ? this.props.items[i - 1].timestamp : 0); 93 | return ( 94 | 106 | ); 107 | })} 108 | 109 |
110 | {this.state.howManyToRender > this.state.pageSize && 111 | 112 | 113 | 114 | } 115 | {this.props.items.length > this.state.howManyToRender && 116 | 117 | 118 | 119 | } 120 |
121 |
122 | ); 123 | } 124 | } 125 | 126 | export default AlertItems; 127 | -------------------------------------------------------------------------------- /src/components/alerts/AlertSection.css: -------------------------------------------------------------------------------- 1 | .AlertSection { 2 | margin-bottom: 30px; 3 | } 4 | 5 | /* history */ 6 | 7 | .history-summary { 8 | position: relative; 9 | margin-top: 15px; 10 | clear: both; 11 | } 12 | 13 | .history-summary-title { 14 | font-size: 0.8em; 15 | color: #bbb; 16 | } 17 | 18 | .history-chart-title { 19 | text-align: center; 20 | font-size: 1em; 21 | color: #555; 22 | } -------------------------------------------------------------------------------- /src/components/alerts/QuietFor.css: -------------------------------------------------------------------------------- 1 | .QuietFor { 2 | padding: 8px 5px; 3 | margin: 10px 0px; 4 | text-align: left; 5 | background-color: #171717; 6 | border-radius: 5px; 7 | font-size: 0.9em; 8 | border: 0px solid #464646; 9 | color: #bbb; 10 | } 11 | 12 | .QuietFor.quietfor-normal-size { 13 | 14 | } 15 | .QuietFor.quietfor-medium-size { 16 | padding-top: 25px; 17 | padding-bottom: 25px; 18 | } 19 | .QuietFor.quietfor-large-size { 20 | padding-top: 50px; 21 | padding-bottom: 50px; 22 | } 23 | .QuietFor.quietfor-xlarge-size { 24 | padding-top: 100px; 25 | padding-bottom: 100px; 26 | } 27 | 28 | .QuietFor.color-green { 29 | border: 1px solid #004c00; 30 | background-color: #001d00; 31 | } 32 | .QuietFor.color-yellow { 33 | border: 1px solid #8e8e00; 34 | background-color: #333300; 35 | } 36 | .QuietFor.color-orange { 37 | border: 1px solid #805300; 38 | background-color: #2f1f00; 39 | } 40 | .QuietFor.color-red { 41 | border: 1px solid #900000; 42 | background-color: #2b0000; 43 | } 44 | 45 | .QuietForIcon { 46 | float: right; 47 | position: relative; 48 | top: 2px; 49 | opacity: 1; 50 | font-size: 1em; 51 | margin-right: 5px; 52 | } 53 | .QuietForClock { 54 | margin-left: 6px; 55 | margin-right: 9px; 56 | } -------------------------------------------------------------------------------- /src/components/alerts/QuietFor.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Component } from 'react'; 20 | import { translate } from '../../helpers/language'; 21 | // icons 22 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 23 | import { faClock, faCloudShowersHeavy, faCloudSunRain, faCloudSun, faSun } from '@fortawesome/free-solid-svg-icons'; 24 | 25 | import './QuietFor.css'; 26 | 27 | interface QuietForProps { 28 | nowtime: number; 29 | prevtime: number; 30 | language: string; 31 | } 32 | 33 | class QuietFor extends Component { 34 | 35 | shouldComponentUpdate(nextProps: QuietForProps) { 36 | //console.log('shouldComponentUpdate', nextProps, nextState); 37 | if (nextProps.nowtime !== this.props.nowtime || nextProps.prevtime !== this.props.prevtime) { 38 | return true; 39 | } else { 40 | return false; 41 | } 42 | } 43 | 44 | render() { 45 | 46 | const { language } = this.props; 47 | 48 | const quietForText = (date_now: number, date_future: number) => { 49 | //var diff = date_now - date_future; 50 | //var total_minutes = (diff/(60*1000)).toFixed(0); 51 | 52 | // calculate days, hours, minutes, seconds 53 | // get total seconds between the times 54 | let delta = Math.abs(date_future - date_now) / 1000; 55 | //console.log('QuietFor render() delta', delta); 56 | 57 | // calculate (and subtract) whole days 58 | const days = Math.floor(delta / 86400); 59 | delta -= days * 86400; 60 | 61 | // calculate (and subtract) whole hours 62 | const hours = Math.floor(delta / 3600) % 24; 63 | delta -= hours * 3600; 64 | 65 | // calculate (and subtract) whole minutes 66 | const minutes = Math.floor(delta / 60) % 60; 67 | delta -= minutes * 60; 68 | 69 | // what's left is seconds 70 | const seconds = parseInt((delta % 60).toFixed(0), 10); // in theory the modulus is not required 71 | 72 | let foo = ''; 73 | if (days === 1) { foo += days + ' ' + translate('day', language) + ' '; } 74 | if (days > 1) { foo += days + ' ' + translate('days', language) + ' '; } 75 | if (hours === 1) { foo += hours + ' ' + translate('hour', language) + ' '; } 76 | if (hours > 1) { foo += hours + ' ' + translate('hours', language) + ' '; } 77 | if (minutes === 1) { foo += minutes + ' ' + translate('minute', language) + ' '; } 78 | if (minutes > 1) { foo += minutes + ' ' + translate('minutes', language) + ' '; } 79 | if (days === 0 && hours === 0 && minutes === 0) { 80 | if (seconds === 1) { 81 | foo += seconds + ' ' + translate('second', language) + ''; 82 | } else { 83 | foo += seconds + ' ' + translate('seconds', language) + ''; 84 | } 85 | } 86 | return foo; 87 | }; 88 | 89 | const date_future = this.props.prevtime; 90 | const date_now = this.props.nowtime; 91 | const durationMs = Math.abs(date_future - date_now); 92 | const hours = Math.floor(durationMs / (1000 * 60 * 60)); 93 | 94 | let icon = ''; 95 | let color = 'color-white'; 96 | let size = 'quietfor-normal-size' 97 | // if (hours > 12) { 98 | // icon = ; 99 | // color = 'color-green'; 100 | // size = 'quietfor-xlarge-size'; 101 | // } else if (hours > 6 && hours <= 12) { 102 | // icon = ; 103 | // color = 'color-yellow'; 104 | // size = 'quietfor-large-size'; 105 | // } else if (hours > 1 && hours <= 6) { 106 | // icon = ; 107 | // color = 'color-orange'; 108 | // size = 'quietfor-medium-size'; 109 | // } else { 110 | // icon = ; 111 | // color = 'color-red'; 112 | // } 113 | 114 | //console.log('quietFor render'); 115 | return ( 116 |
117 |
{icon}
118 | 119 | 120 | 121 | {translate('quiet', language)} {quietForText(this.props.nowtime, this.props.prevtime)} 122 |
123 | ); 124 | } 125 | } 126 | 127 | export default QuietFor; 128 | -------------------------------------------------------------------------------- /src/components/animation.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | .example-enter { 20 | opacity: 0.01; 21 | max-height: 0px; 22 | } 23 | 24 | .example-enter.example-enter-active { 25 | opacity: 1; 26 | max-height: 75px; 27 | transition: all 500ms ease-in; 28 | } 29 | 30 | .example-exit { 31 | opacity: 1; 32 | max-height: 75px; 33 | } 34 | 35 | .example-exit.example-exit-active { 36 | opacity: 0.01; 37 | max-height: 0px; 38 | transition: all 500ms ease-in; 39 | } -------------------------------------------------------------------------------- /src/components/hosts/HostFilters.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/src/components/hosts/HostFilters.css -------------------------------------------------------------------------------- /src/components/hosts/HostGroupFilter.css: -------------------------------------------------------------------------------- 1 | .HostGroupFilter { 2 | margin-top: 10px; 3 | margin-bottom: 15px; 4 | font-size: 1.1em; 5 | color: #bbb; 6 | } 7 | 8 | .HostGroupFilter select { 9 | background-color: #353535; 10 | border: 1px solid #666; 11 | color: white; 12 | font-size: 0.9em; 13 | white-space: nowrap; 14 | opacity: 0.8; 15 | margin-right: 6px; 16 | position: relative; 17 | top: -1px; 18 | } -------------------------------------------------------------------------------- /src/components/hosts/HostGroupFilter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React, { ChangeEvent } from 'react'; 20 | // State Management 21 | import { useAtom, useAtomValue } from 'jotai'; 22 | import { clientSettingsAtom } from '../../atoms/settingsState'; 23 | import { hostgroupAtom } from '../../atoms/hostgroupAtom'; 24 | import './HostGroupFilter.css'; 25 | import { saveLocalStorage } from 'helpers/nagiostv'; 26 | 27 | // http://pi4.local/nagios/jsonquery.html 28 | // http://pi4.local/nagios/cgi-bin/objectjson.cgi?query=hostgrouplist&details=true 29 | 30 | const HostGroupFilter = () => { 31 | 32 | //const bigState = useAtomValue(bigStateAtom); 33 | const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom); 34 | const hostgroupState = useAtomValue(hostgroupAtom); 35 | 36 | const hostgroup = hostgroupState.response; 37 | const hostgroupFilter = clientSettings.hostgroupFilter; 38 | 39 | const onChangeHostGroupFilter = (e: ChangeEvent) => { 40 | setClientSettings(curr => { 41 | saveLocalStorage('HostGroup Filter', { 42 | ...curr, 43 | hostgroupFilter: e.target.value 44 | }); 45 | return ({ 46 | ...curr, 47 | hostgroupFilter: e.target.value 48 | }); 49 | }); 50 | 51 | }; 52 | 53 | if (!hostgroup) { 54 | return (
Could not load hostgroups
); 55 | } 56 | 57 | const keys = Object.keys(hostgroup); 58 | // add an option for each hostgroup returned by the server 59 | const options = keys.map((key, i) => { 60 | return ; 61 | }); 62 | // if the saved hostgroupFilter setting is not in the list of hostgroups from the server, add it manually 63 | if (hostgroupFilter && keys.indexOf(hostgroupFilter) === -1) { 64 | options.push(); 65 | } 66 | 67 | return ( 68 |
69 | HostGroup Filter: {' '} 70 | 74 |
75 | ); 76 | 77 | } 78 | 79 | function propsAreEqual() { 80 | // return Object.keys(prevProps.hostgroup).length === Object.keys(nextProps.hostgroup).length && 81 | // prevProps.hostgroupFilter === nextProps.hostgroupFilter; 82 | return true; 83 | } 84 | 85 | export default React.memo(HostGroupFilter, propsAreEqual); 86 | -------------------------------------------------------------------------------- /src/components/hosts/HostItem.css: -------------------------------------------------------------------------------- 1 | .HostItem { 2 | position: relative; 3 | cursor: pointer; 4 | overflow: hidden; 5 | } 6 | 7 | .host-item-soft { 8 | opacity: 0.6; 9 | } 10 | 11 | .host-item-state-type-0 { 12 | color: #bbb; 13 | font-size: 0.7em; 14 | background-color: #111; 15 | padding: 2px 3px; 16 | border-radius: 4px; 17 | position: relative; 18 | top: -2px; 19 | } 20 | 21 | .host-item-state-type-1 { 22 | font-size: 0.7em; 23 | background-color: #111; 24 | padding: 2px 3px; 25 | border-radius: 4px; 26 | position: relative; 27 | top: -2px; 28 | } 29 | 30 | .host-item-host-name { 31 | display: inline-block; 32 | } -------------------------------------------------------------------------------- /src/components/hosts/HostItems.css: -------------------------------------------------------------------------------- 1 | .HostItems { 2 | margin-top: 10px; 3 | } 4 | 5 | .HostItem { 6 | margin-bottom: 8px; 7 | } 8 | 9 | .HostItem:last-of-type { 10 | margin-bottom: 0px; 11 | } 12 | 13 | .HostItemBorder { 14 | padding: 5px; 15 | padding-left: 7px; 16 | padding-right: 7px; 17 | border-radius: 5px; 18 | min-height: 33px; 19 | padding-bottom: 10px; 20 | } 21 | 22 | .HostItemBorder:last-of-type {} -------------------------------------------------------------------------------- /src/components/hosts/HostItems.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // State Management 20 | import { useAtomValue } from 'jotai'; 21 | import { 22 | //hostIsFetchingAtom, 23 | //hostAtom, 24 | hostHowManyAtom 25 | } from '../../atoms/hostAtom'; 26 | import { commentlistAtom } from '../../atoms/commentlistAtom'; 27 | 28 | import { translate } from '../../helpers/language'; 29 | import HostItem from './HostItem'; 30 | 31 | // icons 32 | //import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 33 | //import { faSun } from '@fortawesome/free-solid-svg-icons'; 34 | 35 | // CSS 36 | import './HostItems.css'; 37 | import { useRef } from 'react'; 38 | import { Host } from 'types/hostAndServiceTypes'; 39 | import { ClientSettings } from 'types/settings'; 40 | 41 | import { AnimatePresence } from "motion/react"; 42 | import * as motion from "motion/react-client"; 43 | 44 | interface HostItemsProps { 45 | hostProblemsArray: Host[]; 46 | settings: ClientSettings; 47 | isDemoMode: boolean; 48 | } 49 | 50 | const HostItems = ({ 51 | hostProblemsArray, 52 | settings, 53 | isDemoMode, 54 | }: HostItemsProps) => { 55 | 56 | const commentlistState = useAtomValue(commentlistAtom); 57 | const commentlistObject = commentlistState.commentlistObject; 58 | 59 | const hostHowManyState = useAtomValue(hostHowManyAtom); 60 | 61 | const { 62 | howManyHosts, 63 | // howManyHostPending, 64 | // howManyHostUp, 65 | // howManyHostDown, 66 | // howManyHostUnreachable, 67 | // howManyHostAcked, 68 | // howManyHostScheduled, 69 | // howManyHostFlapping, 70 | // howManyHostSoft, 71 | // howManyHostNotificationsDisabled, 72 | } = hostHowManyState; 73 | 74 | 75 | 76 | //console.log('hostProblemsArray is', hostProblemsArray); 77 | //console.log(Object.keys(hostProblemsArray)); 78 | 79 | const filteredHostProblemsArray = hostProblemsArray.filter(item => { 80 | if (settings.hideHostPending) { 81 | if (item.status === 1) { return false; } 82 | } 83 | if (settings.hideHostDown) { 84 | if (item.status === 4) { return false; } 85 | } 86 | if (settings.hideHostUnreachable) { 87 | if (item.status === 8) { return false; } 88 | } 89 | if (settings.hideHostAcked) { 90 | if (item.problem_has_been_acknowledged) { return false; } 91 | } 92 | if (settings.hideHostScheduled) { 93 | if (item.scheduled_downtime_depth > 0) { return false; } 94 | } 95 | if (settings.hideHostFlapping) { 96 | if (item.is_flapping) { return false; } 97 | } 98 | if (settings.hideHostSoft) { 99 | if (item.state_type === 0) { return false; } 100 | } 101 | if (settings.hideHostNotificationsDisabled) { 102 | if (item.notifications_enabled === false) { return false; } 103 | } 104 | return true; 105 | }); 106 | 107 | const howManyHidden = hostProblemsArray.length - filteredHostProblemsArray.length; 108 | const showSomeDownItems = hostProblemsArray.length > 0 && filteredHostProblemsArray.length === 0; 109 | const { language } = settings; 110 | 111 | return ( 112 |
113 | 114 | 115 | {hostProblemsArray.length === 0 && 121 | {translate('All', language)} {howManyHosts} {translate('hosts are UP', language)}{' '} 122 | } 123 | 124 | 125 |
126 |
127 | {howManyHosts - hostProblemsArray.length} of {howManyHosts} {translate('hosts are UP', language)}{' '} 128 | {howManyHidden} hidden 129 |
130 |
131 | 132 |
133 | 134 | {filteredHostProblemsArray.map((e, i) => { 135 | //console.log('HostItem item'); 136 | //console.log(e, i); 137 | 138 | return ( 139 | 147 | 154 | 155 | ); 156 | 157 | })} 158 | 159 |
160 |
161 | ); 162 | 163 | }; 164 | 165 | export default HostItems; 166 | -------------------------------------------------------------------------------- /src/components/hosts/host-functions.ts: -------------------------------------------------------------------------------- 1 | import { HostList } from "types/hostAndServiceTypes"; 2 | 3 | export function howManyHostCounter(hostlist: HostList, totalCount: React.MutableRefObject) { 4 | 5 | const howManyHosts = totalCount.current; 6 | 7 | let howManyHostPending = 0; 8 | let howManyHostUp = 0; 9 | let howManyHostDown = 0; 10 | let howManyHostUnreachable = 0; 11 | let howManyHostAcked = 0; 12 | let howManyHostScheduled = 0; 13 | let howManyHostFlapping = 0; 14 | let howManyHostSoft = 0; 15 | let howManyHostNotificationsDisabled = 0; 16 | 17 | if (hostlist) { 18 | Object.keys(hostlist).forEach((host) => { 19 | 20 | if (hostlist[host].status === 1) { 21 | howManyHostPending++; 22 | } 23 | if (hostlist[host].status === 4) { 24 | howManyHostDown++; 25 | } 26 | if (hostlist[host].status === 8) { 27 | howManyHostUnreachable++; 28 | } 29 | if (hostlist[host].problem_has_been_acknowledged) { 30 | howManyHostAcked++; 31 | } 32 | if (hostlist[host].scheduled_downtime_depth > 0) { 33 | howManyHostScheduled++; 34 | } 35 | if (hostlist[host].is_flapping) { 36 | howManyHostFlapping++; 37 | } 38 | // only count soft items if they are not up 39 | if (hostlist[host].status !== 2 && hostlist[host].state_type === 0) { 40 | howManyHostSoft++; 41 | } 42 | // count notifications_enabled === false 43 | // only count these if they are not up 44 | if (hostlist[host].status !== 2 && hostlist[host].notifications_enabled === false) { 45 | howManyHostNotificationsDisabled++; 46 | } 47 | }); 48 | 49 | howManyHostUp = howManyHosts - howManyHostDown - howManyHostUnreachable; 50 | } 51 | 52 | return { 53 | howManyHosts, 54 | howManyHostPending, 55 | howManyHostUp, 56 | howManyHostDown, 57 | howManyHostUnreachable, 58 | howManyHostAcked, 59 | howManyHostScheduled, 60 | howManyHostFlapping, 61 | howManyHostSoft, 62 | howManyHostNotificationsDisabled, 63 | }; 64 | } -------------------------------------------------------------------------------- /src/components/panels/BottomPanel.css: -------------------------------------------------------------------------------- 1 | .BottomPanel { 2 | /* 3 | position: fixed; 4 | bottom: 0px; 5 | left: 50%; 6 | transform: translateX(-50%); 7 | border-top-left-radius: 5px; 8 | border-top-right-radius: 5px; 9 | border: 2px solid #111; 10 | border-bottom: none; 11 | background-color: #222222d1; 12 | z-index: 13; 13 | text-align: center; 14 | font-size: 0.9em; 15 | white-space: nowrap; 16 | */ 17 | } 18 | 19 | .BottomPanel .nav-sidebar-icon { 20 | margin-left: 2px; 21 | margin-right: 2px; 22 | } 23 | 24 | 25 | .BottomPanel>.bottom-panel-area { 26 | position: fixed; 27 | bottom: 0px; 28 | left: 50%; 29 | transform: translateX(-50%); 30 | z-index: 13; 31 | 32 | background-color: #222222; 33 | border: 1px solid #444; 34 | border-bottom: 0; 35 | border-top-left-radius: 5px; 36 | border-top-right-radius: 5px; 37 | 38 | text-align: center; 39 | display: inline-block; 40 | white-space: nowrap; 41 | vertical-align: middle; 42 | 43 | font-size: 0.9em; 44 | color: #999; 45 | cursor: pointer; 46 | } 47 | 48 | .BottomPanel>.bottom-panel-area>.bottom-panel-area-text { 49 | 50 | background-color: #111; 51 | border: 0px solid #444; 52 | border-radius: 5px; 53 | 54 | margin: 3px 3px; 55 | text-align: center; 56 | display: inline-block; 57 | white-space: nowrap; 58 | vertical-align: middle; 59 | 60 | padding: 2px 5px; 61 | font-size: 0.9em; 62 | color: #999; 63 | cursor: pointer; 64 | } 65 | 66 | .bottom-panel-nav-area { 67 | position: fixed; 68 | bottom: -56px; 69 | left: 50%; 70 | transform: translateX(-50%); 71 | z-index: 12; 72 | 73 | height: 49px; 74 | transition: bottom 250ms linear, opacity 350ms linear; 75 | 76 | border: 2px solid #333; 77 | border-radius: 5px; 78 | /* 79 | border-bottom: 0; 80 | border-top-left-radius: 5px; 81 | border-top-right-radius: 5px; 82 | */ 83 | text-align: center; 84 | display: inline-block; 85 | white-space: nowrap; 86 | vertical-align: middle; 87 | background-color: #111; 88 | padding: 2px 5px; 89 | font-size: 0.9em; 90 | color: #999; 91 | opacity: 0; 92 | } 93 | 94 | .bottom-panel-nav-area-visible { 95 | bottom: 29px; 96 | opacity: 1; 97 | } 98 | 99 | 100 | .BottomPanel .current-version:hover { 101 | color: orange; 102 | } 103 | 104 | 105 | .BottomPanel .nav-sidebar-icon-spacer { 106 | display: inline-block; 107 | border-right: 2px solid #333; 108 | height: 30px; 109 | vertical-align: middle; 110 | margin-left: 6px; 111 | margin-right: 4px; 112 | } 113 | 114 | .BottomPanel .nav-sidebar-icon { 115 | position: relative; 116 | width: 50px; 117 | height: 50px; 118 | border: 0px solid #333; 119 | text-align: center; 120 | display: inline-block; 121 | vertical-align: middle; 122 | cursor: pointer; 123 | } 124 | 125 | .BottomPanel .nav-sidebar-icon .nav-sidebar-icon-text { 126 | font-size: 0.8em; 127 | color: #555; 128 | } 129 | 130 | .BottomPanel .nav-sidebar-icon span { 131 | position: absolute; 132 | top: 50%; 133 | left: 50%; 134 | transform: translate(-50%, -50%); 135 | } 136 | 137 | .BottomPanel .nav-sidebar-icon svg { 138 | font-size: 1.2em; 139 | color: #666; 140 | } 141 | 142 | /* 143 | .BottomPanel .nav-sidebar-icon svg.nav-sidebar-icon-selected { 144 | color: #eee; 145 | } 146 | */ 147 | 148 | .nav-sidebar-icon-error { 149 | animation: error-blink 2s infinite alternate; 150 | } 151 | 152 | @keyframes error-blink { 153 | 0% { 154 | color: red; 155 | } 156 | 157 | 50% { 158 | color: orange; 159 | } 160 | 161 | 100% { 162 | color: yellow; 163 | } 164 | } 165 | 166 | .BottomPanel .nav-sidebar-number { 167 | margin-top: 20px; 168 | color: orange; 169 | font-size: 1.4em; 170 | text-align: center; 171 | } 172 | 173 | .nav-sidebar-bottom-float { 174 | position: absolute; 175 | bottom: 0; 176 | } 177 | 178 | /* styles for react router links */ 179 | 180 | .BottomPanel a { 181 | text-decoration: none; 182 | } 183 | 184 | .BottomPanel .is-active>svg { 185 | color: #eee !important; 186 | font-size: 0.9em !important; 187 | } 188 | 189 | .BottomPanel .is-active>.nav-sidebar-icon-text { 190 | color: #eee; 191 | } 192 | 193 | /* more footer */ 194 | 195 | .update-available { 196 | padding: 1px 8px; 197 | border-radius: 5px; 198 | } 199 | 200 | .update-available a { 201 | color: #a7a749; 202 | text-decoration: none; 203 | cursor: pointer; 204 | } 205 | 206 | .update-available a:hover { 207 | color: orange; 208 | } 209 | 210 | .bottom-panel-nagiostv-brand { 211 | position: fixed; 212 | bottom: 8px; 213 | left: 50%; 214 | transform: translateX(-50%); 215 | color: #444; 216 | font-size: 0.7em; 217 | 218 | } -------------------------------------------------------------------------------- /src/components/panels/LeftPanel.css: -------------------------------------------------------------------------------- 1 | .LeftPanel { 2 | position: fixed; 3 | top: 40px; 4 | left: -50px; 5 | width: 50px; 6 | border: 0px solid #333; 7 | background-color: #1d1d1d; 8 | z-index: 13; 9 | overflow: hidden; 10 | transition: left 250ms linear; 11 | } 12 | 13 | .LeftPanel.left-panel-open { 14 | left: 0px; 15 | } 16 | 17 | .LeftPanel .nav-sidebar-icon { 18 | 19 | width: 50px; 20 | height: 50px; 21 | border: 0px solid #333; 22 | text-align: center; 23 | font-size: 1.2em; 24 | color: #666; 25 | 26 | display: grid; 27 | place-content: center; 28 | } 29 | 30 | .LeftPanel .nav-sidebar-icon svg { 31 | color: #666; 32 | 33 | } 34 | 35 | .nav-sidebar-icon-error { 36 | animation: error-blink 2s infinite alternate; 37 | } 38 | 39 | @keyframes error-blink { 40 | 0% { 41 | color: red; 42 | } 43 | 44 | 50% { 45 | color: orange; 46 | } 47 | 48 | 100% { 49 | color: yellow; 50 | } 51 | } 52 | 53 | .nav-sidebar-icon-selected { 54 | color: #eee; 55 | } 56 | 57 | .LeftPanel .nav-sidebar-item { 58 | position: static; 59 | width: 50px; 60 | height: 50px; 61 | border: 0px solid #333; 62 | text-align: center; 63 | } 64 | 65 | .LeftPanel .nav-sidebar-number { 66 | margin-top: 20px; 67 | color: orange; 68 | font-size: 1.4em; 69 | text-align: center; 70 | } 71 | 72 | .nav-sidebar-bottom-float { 73 | position: absolute; 74 | bottom: 0; 75 | } 76 | 77 | .nav-sidebar-hr { 78 | border-bottom: 1px solid #555; 79 | margin: 0 10px 10px 10px; 80 | } 81 | 82 | /* styles for react router links */ 83 | 84 | .LeftPanel a { 85 | text-decoration: none; 86 | } 87 | 88 | .LeftPanel .is-active>svg { 89 | color: #eee !important; 90 | font-size: 0.9em !important; 91 | } 92 | 93 | .LeftPanel .is-active>.nav-sidebar-icon-text { 94 | color: #eee; 95 | } -------------------------------------------------------------------------------- /src/components/panels/LeftPanel.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // React Router 20 | import { NavLink } from "react-router-dom"; 21 | 22 | import './LeftPanel.css'; 23 | 24 | //import ReactTooltip from 'react-tooltip'; 25 | 26 | // icons 27 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 28 | import { faTachometerAlt, faTools, faUpload, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; 29 | import { useSetAtom } from "jotai"; 30 | import { bigStateAtom } from "atoms/settingsState"; 31 | 32 | interface LeftPanelProps { 33 | isLeftPanelOpen: boolean; 34 | } 35 | 36 | const LeftPanel = ({ 37 | isLeftPanelOpen 38 | }: LeftPanelProps) => { 39 | 40 | const setBigState = useSetAtom(bigStateAtom); 41 | 42 | const clickedItem = () => { 43 | //console.log('clicked'); 44 | setBigState(curr => ({ 45 | ...curr, 46 | isLeftPanelOpen: false, 47 | })); 48 | } 49 | 50 | return ( 51 |
52 | 53 |
54 | 55 | (isActive ? 'is-active' : '')} to="/"> 56 | 61 | 62 | 63 |
64 | 65 |
66 | 67 | (isActive ? 'is-active' : '')} to="/settings"> 68 | 73 | 74 | 75 |
76 | 77 |
78 | 79 | (isActive ? 'is-active' : '')} to="/update"> 80 | 85 | 86 | 87 |
88 | 89 |
90 | 91 | (isActive ? 'is-active' : '')} to="/help"> 92 | 97 | 98 | 99 |
100 | 101 |
102 | 103 |
104 | ); 105 | } 106 | 107 | export default LeftPanel; 108 | -------------------------------------------------------------------------------- /src/components/panels/RightPanel.css: -------------------------------------------------------------------------------- 1 | .RightPanel { 2 | position: fixed; 3 | top: 40px; 4 | bottom: 0px; 5 | right: -120px; 6 | width: 120px; 7 | border: 0px solid #333; 8 | /* background-color: #1d1d1d; */ 9 | z-index: 13; 10 | overflow: hidden; 11 | /* transition: right 250ms linear; */ 12 | } 13 | 14 | .RightPanel.right-panel-open { 15 | right: 0px; 16 | } -------------------------------------------------------------------------------- /src/components/panels/RightPanel.tsx: -------------------------------------------------------------------------------- 1 | import MiniMapCanvas from '../widgets/MiniMapCanvas'; 2 | import { useLocation } from "react-router-dom"; 3 | import './RightPanel.css'; 4 | 5 | interface RightPanelProps { 6 | isRightPanelOpen: boolean; 7 | showMiniMap: boolean; 8 | miniMapWidth: number; 9 | } 10 | 11 | const RightPanel = ({ 12 | isRightPanelOpen, 13 | showMiniMap, 14 | miniMapWidth, 15 | }: RightPanelProps) => { 16 | 17 | // React router location 18 | const location = useLocation(); 19 | 20 | let whichElementToSnapshot = '.Dashboard'; 21 | if (location.pathname === '/settings') { 22 | whichElementToSnapshot = '.Settings'; 23 | } 24 | if (location.pathname === '/update') { 25 | whichElementToSnapshot = '.Update'; 26 | } 27 | if (location.pathname === '/help') { 28 | whichElementToSnapshot = '.Help'; 29 | } 30 | 31 | return ( 32 |
36 | {showMiniMap && ( 37 | 41 | )} 42 |
43 | ); 44 | }; 45 | 46 | export default RightPanel; -------------------------------------------------------------------------------- /src/components/panels/TopPanel.css: -------------------------------------------------------------------------------- 1 | /* TopPanel.css */ 2 | 3 | .top-panel-height { 4 | height: 40px; 5 | } 6 | 7 | .TopPanel { 8 | position: fixed; 9 | top: 0; 10 | left: 0; 11 | right: 0; 12 | z-index: 10; 13 | background-color: #181b1f; 14 | border-bottom: 1px solid rgba(204, 204, 220, .07); 15 | } 16 | 17 | .TopPanel>.header-right-float { 18 | position: absolute; 19 | top: 0; 20 | right: 0px; 21 | height: 40px; 22 | vertical-align: top; 23 | margin-right: 15px; 24 | } 25 | 26 | .TopPanel>.header-application-name { 27 | position: absolute; 28 | top: 50%; 29 | transform: translateY(-50%); 30 | margin-left: 15px; 31 | margin-right: 10px; 32 | display: inline-block; 33 | color: #ddd; 34 | -webkit-user-select: none; 35 | user-select: none; 36 | } 37 | 38 | .TopPanel .generic-icon { 39 | display: inline-block; 40 | vertical-align: top; 41 | position: relative; 42 | top: 50%; 43 | transform: translateY(-50%); 44 | color: #ccc; 45 | margin-right: 15px; 46 | cursor: pointer; 47 | } 48 | 49 | .TopPanel .generic-icon.generic-icon-disabled { 50 | color: #666; 51 | } 52 | 53 | .TopPanel .generic-icon.filter-icon { 54 | /* color: rgb(211, 151, 22); */ 55 | } 56 | 57 | .TopPanel .generic-icon.filter-icon.generic-icon-disabled { 58 | /* color: rgb(139, 118, 71);; */ 59 | } 60 | 61 | /* if we have the hamburger menu then we do not need the extra left margin */ 62 | .TopPanel>.hamburger-menu+.header-application-name { 63 | margin-left: 0px; 64 | } 65 | 66 | .TopPanel>.hamburger-menu { 67 | font-size: 1.2em; 68 | display: inline-block; 69 | 70 | height: 40px; 71 | width: 45px; 72 | color: #666; 73 | transition: color 250ms linear; 74 | cursor: pointer; 75 | } 76 | 77 | .TopPanel>.hamburger-menu>.hamburger-menu-center { 78 | height: 40px; 79 | width: 50px; 80 | display: flex; 81 | flex-direction: column; 82 | align-items: center; 83 | justify-content: center; 84 | gap: 1ch; 85 | } 86 | 87 | .TopPanel>.hamburger-menu.hamburger-menu-active { 88 | color: #fff; 89 | } 90 | 91 | .TopPanel>.hamburger-menu:hover { 92 | color: orange; 93 | } 94 | 95 | .automatic-scroll-enabled { 96 | position: fixed; 97 | bottom: 10px; 98 | right: 10px; 99 | width: auto; 100 | background-color: #333; 101 | border: 1px solid #444; 102 | border-radius: 5px; 103 | padding: 5px 5px; 104 | z-index: 10; 105 | } -------------------------------------------------------------------------------- /src/components/services/ServiceFilters.css: -------------------------------------------------------------------------------- 1 | /* filters */ 2 | 3 | .vertical-scroll-dash select { 4 | background-color: #353535; 5 | border: 1px solid #404040; 6 | color: white; 7 | font-size: 1em; 8 | white-space: nowrap; 9 | opacity: 0.8; 10 | margin-right: 6px; 11 | position: relative; 12 | top: -1px; 13 | } -------------------------------------------------------------------------------- /src/components/services/ServiceGroupFilter.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriscareycode/nagiostv-react/400b496b11f68641ac2034c8dfb8c19860c0e029/src/components/services/ServiceGroupFilter.css -------------------------------------------------------------------------------- /src/components/services/ServiceGroupFilter.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import React, { ChangeEvent } from 'react'; 20 | // State Management 21 | import { useAtom, useAtomValue } from 'jotai'; 22 | import { clientSettingsAtom } from '../../atoms/settingsState'; 23 | import { servicegroupAtom } from '../../atoms/hostgroupAtom'; 24 | import './ServiceGroupFilter.css'; 25 | import { saveLocalStorage } from 'helpers/nagiostv'; 26 | 27 | // http://pi4.local/nagios/jsonquery.html 28 | // http://pi4.local/nagios/cgi-bin/objectjson.cgi?query=servicegrouplist&details=true 29 | 30 | const ServiceGroupFilter = () => { 31 | 32 | //const bigState = useAtomValue(bigStateAtom); 33 | const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom); 34 | const servicegroupState = useAtomValue(servicegroupAtom); 35 | 36 | const servicegroup = servicegroupState.response; 37 | const servicegroupFilter = clientSettings.servicegroupFilter; 38 | 39 | const onChangeServiceGroupFilter = (e: ChangeEvent) => { 40 | setClientSettings(curr => { 41 | saveLocalStorage('Service Group Filter', { 42 | ...curr, 43 | servicegroupFilter: e.target.value 44 | }); 45 | return ({ 46 | ...curr, 47 | servicegroupFilter: e.target.value 48 | }); 49 | }); 50 | 51 | }; 52 | 53 | if (!servicegroup) { 54 | return (
Could not load servicegroups
); 55 | } 56 | 57 | const keys = Object.keys(servicegroup); 58 | // add an option for each servicegroup returned by the server 59 | const options = keys.map((key, i) => { 60 | return ; 61 | }); 62 | // if the saved servicegroupFilter setting is not in the list of servicegroups from the server, add it manually 63 | if (servicegroupFilter && keys.indexOf(servicegroupFilter) === -1) { 64 | options.push(); 65 | } 66 | 67 | return ( 68 |
69 | ServiceGroup Filter: {' '} 70 | 74 |
75 | ); 76 | 77 | } 78 | 79 | function propsAreEqual() { 80 | // return Object.keys(prevProps.hostgroup).length === Object.keys(nextProps.hostgroup).length && 81 | // prevProps.hostgroupFilter === nextProps.hostgroupFilter; 82 | return true; 83 | } 84 | 85 | export default React.memo(ServiceGroupFilter, propsAreEqual); 86 | -------------------------------------------------------------------------------- /src/components/services/ServiceItem.css: -------------------------------------------------------------------------------- 1 | .ServiceItem { 2 | position: relative; 3 | cursor: pointer; 4 | overflow: hidden; 5 | } 6 | 7 | .comment { 8 | margin-top: 0px; 9 | font-size: 0.8em; 10 | /* 11 | background-color: rgba(0, 0, 0, 0.3); 12 | margin-bottom: -5px; 13 | margin-left: -5px; 14 | margin-right: -5px; 15 | padding: 2px 4px 4px 4px; 16 | border-bottom-right-radius: 5px; 17 | border-bottom-left-radius: 5px; 18 | border-radius: 5px; 19 | border-top: 1px solid #333; 20 | border-left: 1px solid #333; 21 | border-right: 1px solid #333; 22 | */ 23 | display: inline-block 24 | } 25 | 26 | .comment-color { 27 | color: #e27cfb; 28 | /* purple */ 29 | color: #fbdba0; 30 | /* peach */ 31 | color: lime; 32 | /* to match with ACKED green. Since it's a good thing */ 33 | color: #7cfd7c; 34 | /* soft/light green */ 35 | color: #fdee7c; 36 | color: #fdfdfd; 37 | /* whitish */ 38 | } 39 | 40 | .checking-now { 41 | margin-left: 0px; 42 | /*color: yellow;*/ 43 | } 44 | 45 | /* soft */ 46 | .service-item-soft { 47 | opacity: 0.6; 48 | } 49 | 50 | /* soft */ 51 | .service-item-state-type-0 { 52 | color: #bbb; 53 | font-size: 0.7em; 54 | background-color: #111; 55 | padding: 2px 3px; 56 | border-radius: 4px; 57 | position: relative; 58 | top: -2px; 59 | } 60 | 61 | /* hard */ 62 | .service-item-state-type-1 { 63 | font-size: 0.7em; 64 | background-color: #111; 65 | padding: 2px 3px; 66 | border-radius: 4px; 67 | position: relative; 68 | top: -2px; 69 | } 70 | 71 | .item-notifications-disabled { 72 | font-size: 0.7em; 73 | } 74 | 75 | .service-item-description { 76 | font-size: 1em; 77 | padding: 2px 4px; 78 | border-radius: 4px; 79 | position: relative; 80 | top: 0px; 81 | border: 0px solid #545454; 82 | background-color: #101010; 83 | margin-right: 5px; 84 | margin-left: 5px; 85 | color: #fdee7c; 86 | } 87 | 88 | .service-item-host-name { 89 | display: inline-block; 90 | } 91 | 92 | .service-item-left-first-line { 93 | 94 | } -------------------------------------------------------------------------------- /src/components/services/ServiceItems.css: -------------------------------------------------------------------------------- 1 | .ServiceItems {} 2 | 3 | .ServiceItem { 4 | margin-top: 0px; 5 | margin-bottom: 8px; 6 | } 7 | 8 | .ServiceItem:last-of-type { 9 | margin-bottom: 0px; 10 | } 11 | 12 | .ServiceItemBorder { 13 | padding: 5px; 14 | padding-left: 7px; 15 | padding-right: 7px; 16 | border-radius: 5px; 17 | min-height: 33px; 18 | padding-bottom: 10px; 19 | } 20 | 21 | .ServiceItemError { 22 | border-radius: 5px; 23 | padding: 5px; 24 | color: yellow; 25 | margin-bottom: 10px; 26 | padding-left: 8px; 27 | } 28 | 29 | .last-ok { 30 | font-size: 0.8em; 31 | } 32 | 33 | .next-check-in { 34 | font-size: 0.8em; 35 | } 36 | 37 | /* border */ 38 | 39 | .border-white { 40 | border: 1px solid white; 41 | } 42 | 43 | .border-green { 44 | border: 1px solid #005e00; 45 | border-left: 3px solid lime; 46 | /* border-right: 3px solid lime; */ 47 | background-color: #082308; 48 | } 49 | 50 | .border-yellow { 51 | border: 1px solid #3c3c00; 52 | border-left: 3px solid yellow; 53 | /* border-right: 3px solid yellow; */ 54 | background-color: #252504; 55 | } 56 | 57 | .border-orange { 58 | border: 1px solid #7c5100; 59 | border-left: 3px solid orange; 60 | /* border-right: 3px solid orange; */ 61 | background-color: #332600; 62 | } 63 | 64 | .border-red { 65 | border: 1px solid #7c3838; 66 | border-left: 3px solid #FD7272; 67 | /* border-right: 3px solid #FD7272; */ 68 | background-color: #2c0909; 69 | } 70 | 71 | .border-gray { 72 | border: 1px solid #464646; 73 | border-left: 3px solid gray; 74 | /* border-right: 3px solid gray; */ 75 | } 76 | 77 | .border-purple { 78 | border-left: 3px solid purple; 79 | } 80 | 81 | /* color */ 82 | 83 | .color-white { 84 | color: #eeeff2; 85 | } 86 | 87 | .color-blue { 88 | color: #00adff; 89 | } 90 | 91 | .color-darkblue { 92 | color: #003f5c; 93 | } 94 | 95 | .color-green { 96 | color: lime; 97 | } 98 | 99 | .color-yellow { 100 | color: yellow; 101 | } 102 | 103 | .color-primary { 104 | color: #6fbbf3; 105 | } 106 | 107 | .color-orange { 108 | color: orange; 109 | } 110 | 111 | .color-peach { 112 | color: #fbdba0; 113 | } 114 | 115 | .color-red { 116 | color: #FD7272; 117 | } 118 | 119 | .color-gray { 120 | color: gray; 121 | } 122 | 123 | .color-purple { 124 | color: purple; 125 | } -------------------------------------------------------------------------------- /src/components/services/service-functions.ts: -------------------------------------------------------------------------------- 1 | import { ServiceList } from "types/hostAndServiceTypes"; 2 | 3 | export function howManyServiceCounter(servicelist: ServiceList, totalCount: React.MutableRefObject) { 4 | // count how many items in each of the service states 5 | let howManyServices = 0; 6 | let howManyServicePending = 0; 7 | let howManyServiceWarning = 0; 8 | let howManyServiceUnknown = 0; 9 | let howManyServiceCritical = 0; 10 | let howManyServiceAcked = 0; 11 | let howManyServiceScheduled = 0; 12 | let howManyServiceFlapping = 0; 13 | let howManyServiceSoft = 0; 14 | let howManyServiceNotificationsDisabled = 0; 15 | 16 | if (servicelist) { 17 | Object.keys(servicelist).forEach((host) => { 18 | 19 | // Deprecated now that we are getting the count from another api 20 | howManyServices += Object.keys(servicelist[host]).length; 21 | 22 | Object.keys(servicelist[host]).forEach((service) => { 23 | if (servicelist[host][service].status === 1) { 24 | howManyServicePending++; 25 | } 26 | if (servicelist[host][service].status === 4) { 27 | howManyServiceWarning++; 28 | } 29 | if (servicelist[host][service].status === 8) { 30 | howManyServiceUnknown++; 31 | } 32 | if (servicelist[host][service].status === 16) { 33 | howManyServiceCritical++; 34 | } 35 | if (servicelist[host][service].problem_has_been_acknowledged) { 36 | howManyServiceAcked++; 37 | } 38 | if (servicelist[host][service].scheduled_downtime_depth > 0) { 39 | howManyServiceScheduled++; 40 | } 41 | if (servicelist[host][service].is_flapping) { 42 | howManyServiceFlapping++; 43 | } 44 | // only count soft items if they are not OK state 45 | if (servicelist[host][service].status !== 2 && servicelist[host][service].state_type === 0) { 46 | howManyServiceSoft++; 47 | } 48 | // count notifications_enabled === false 49 | // only count notifications_enabled items if they are not OK state 50 | if (servicelist[host][service].status !== 2 && servicelist[host][service].notifications_enabled === false) { 51 | howManyServiceNotificationsDisabled++; 52 | } 53 | }); 54 | }); 55 | } 56 | 57 | howManyServices = totalCount.current; 58 | 59 | const howManyServiceOk = howManyServices - howManyServiceWarning - howManyServiceCritical - howManyServiceUnknown; 60 | 61 | return { 62 | howManyServices, 63 | howManyServiceOk, 64 | howManyServicePending, 65 | howManyServiceWarning, 66 | howManyServiceUnknown, 67 | howManyServiceCritical, 68 | howManyServiceAcked, 69 | howManyServiceScheduled, 70 | howManyServiceFlapping, 71 | howManyServiceSoft, 72 | howManyServiceNotificationsDisabled, 73 | }; 74 | 75 | } -------------------------------------------------------------------------------- /src/components/summary/MostRecentAlert.tsx: -------------------------------------------------------------------------------- 1 | // Widgets 2 | import AlertItem from 'components/alerts/AlertItem'; 3 | // Helpers 4 | import { formatDateTimeAgoColorQuietFor } from '../../helpers/dates'; 5 | import { useAtomValue } from 'jotai'; 6 | import { alertAtom } from 'atoms/alertAtom'; 7 | import { clientSettingsAtom } from 'atoms/settingsState'; 8 | 9 | const MostRecentAlert = () => { 10 | 11 | const clientSettings = useAtomValue(clientSettingsAtom); 12 | const alertState = useAtomValue(alertAtom); 13 | const alertlist = alertState.responseArray; 14 | 15 | let quietForMs: number | null = null; 16 | if (alertState && alertState.responseArray && alertState.responseArray.length > 0) { 17 | quietForMs = alertState.responseArray[0].timestamp; 18 | } 19 | 20 | return ( 21 | <> 22 | {/* Most Recent Alert Section */} 23 | {alertlist.length > 0 &&
24 |
25 | 26 |
Most recent alert {quietForMs ? formatDateTimeAgoColorQuietFor(quietForMs) : '?'} ago:
27 | 28 | {/*
hard service ok
Sat, Oct 9, 2021 8:30 AM
unicorn Check APTAPT OK: 0 packages available for upgrade (0 critical updates).
*/} 29 | 30 | 42 |
43 |
} 44 | 45 | ); 46 | }; 47 | 48 | export default MostRecentAlert; 49 | -------------------------------------------------------------------------------- /src/components/summary/Summary.css: -------------------------------------------------------------------------------- 1 | .summary-item { 2 | position: relative; 3 | border: 1px solid #002232; 4 | border-left: 3px solid #00adff; 5 | background-color: #051218; 6 | font-size: 1.5em; 7 | border-radius: 5px; 8 | margin-top: 15px; 9 | display: flex; 10 | flex-direction: row; 11 | flex-wrap: nowrap; 12 | justify-content: space-between; 13 | } 14 | 15 | .summary-left-side { 16 | display: flex; 17 | flex-wrap: wrap; /* Allow children within containers to wrap */ 18 | justify-content: flex-start; 19 | } 20 | 21 | .summary-right-side { 22 | display: flex; 23 | flex-wrap: wrap; /* Allow children within containers to wrap */ 24 | justify-content: flex-end; 25 | margin-left: auto; /* Pushes the entire container to the right */ 26 | align-items: flex-start; /* Align items to the top */ 27 | } 28 | 29 | .font-size-small { 30 | font-size: 0.6em; 31 | } 32 | 33 | .no-wrap { 34 | white-space: nowrap; 35 | } 36 | 37 | .overflow-hidden { 38 | overflow: hidden; 39 | } 40 | 41 | .summary-box:first-of-type { 42 | margin-left: 10px; 43 | } 44 | 45 | .summary-box { 46 | display: inline-block; 47 | vertical-align: top; 48 | border: 1px solid #001d2a; 49 | background-color: #000c12; 50 | border-radius: 10px; 51 | text-align: center; 52 | padding: 5px 5px 5px 5px; 53 | margin-left: 5px; 54 | margin-top: 1px; 55 | margin-bottom: 5px; 56 | font-size: 0.8em; 57 | } 58 | 59 | .summary-box-big-number { 60 | font-size: 1.5em; 61 | } 62 | 63 | .summary-box-text { 64 | font-size: 0.6em; 65 | } 66 | 67 | .summary-box-right { 68 | position: absolute; 69 | top: 10px; 70 | right: 10px; 71 | bottom: 10px; 72 | width: 100px; 73 | border: 1px solid #003f5c; 74 | border-radius: 6px; 75 | text-align: center; 76 | padding-top: 15px; 77 | } 78 | 79 | .summary-box.summary-box-separator { 80 | background-color: #02496a; 81 | height: 50px; 82 | width: 2px; 83 | padding: 0; 84 | margin-top: 18px; 85 | } 86 | 87 | @media only screen and (min-device-width: 320px) and (max-device-width: 480px) and (-webkit-min-device-pixel-ratio: 2) { 88 | /* .summary-box.float-right { 89 | display: none; 90 | } */ 91 | /* .summary-box.summary-box-text { 92 | display: none; 93 | } */ 94 | } -------------------------------------------------------------------------------- /src/components/widgets/Clock.css: -------------------------------------------------------------------------------- 1 | .Clock { 2 | display: inline-block; 3 | color: #808080; 4 | vertical-align: top; 5 | position: relative; 6 | top: 50%; 7 | transform: translateY(-50%); 8 | user-select: none; 9 | } 10 | 11 | @media only screen and (max-device-width: 480px) { 12 | .Clock { 13 | display: none; 14 | } 15 | } 16 | 17 | @media only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3) { 18 | 19 | .Clock { 20 | display: none; 21 | } 22 | } -------------------------------------------------------------------------------- /src/components/widgets/Clock.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { useEffect, useRef } from 'react'; 20 | import { formatDateTimeLocale } from '../../helpers/dates'; 21 | import './Clock.css'; 22 | 23 | // icons 24 | //import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 25 | //import { faClock } from '@fortawesome/free-solid-svg-icons'; 26 | 27 | const Clock = ({ 28 | locale, 29 | clockDateFormat, 30 | clockTimeFormat, 31 | }) => { 32 | 33 | const dateRef = useRef(null); 34 | 35 | useEffect( 36 | () => { 37 | //start timer 38 | //console.log('Clock() Start 1s interval'); 39 | const timer = setInterval(() => { 40 | if (dateRef && dateRef.current) { 41 | dateRef.current.innerHTML = 42 | formatDateTimeLocale('now', locale, clockDateFormat) + 43 | ' ' + 44 | formatDateTimeLocale('now', locale, clockTimeFormat); 45 | } 46 | }, 1000); 47 | 48 | return () => { 49 | //stop timer 50 | //console.log('Clock() Stop interval'); 51 | if (timer) { 52 | clearInterval(timer); 53 | } 54 | }; 55 | }, 56 | [locale, clockDateFormat, clockTimeFormat] 57 | ); 58 | 59 | return ( 60 |
61 | ); 62 | 63 | } 64 | 65 | export default Clock; 66 | -------------------------------------------------------------------------------- /src/components/widgets/CustomLogo.css: -------------------------------------------------------------------------------- 1 | .CustomLogo { 2 | display: inline-block; 3 | vertical-align: top; 4 | margin-left: 4px; 5 | } 6 | 7 | .CustomLogo img { 8 | height: 32px; 9 | margin-top: 5px; 10 | } -------------------------------------------------------------------------------- /src/components/widgets/CustomLogo.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Component } from 'react'; 20 | import { ClientSettings } from 'types/settings'; 21 | import './CustomLogo.css'; 22 | 23 | interface CustomLogoProps { 24 | settings: ClientSettings; 25 | } 26 | 27 | class CustomLogo extends Component { 28 | 29 | shouldComponentUpdate(nextProps, nextState) { 30 | //console.log('shouldComponentUpdate', nextProps, nextState); 31 | if (nextProps.settings.customLogoEnabled !== this.props.settings.customLogoEnabled || nextProps.settings.customLogoUrl !== this.props.settings.customLogoUrl) { 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | render() { 39 | return ( 40 |
41 | custom logo 42 |
43 | ); 44 | } 45 | } 46 | 47 | export default CustomLogo; 48 | -------------------------------------------------------------------------------- /src/components/widgets/FilterCheckbox.css: -------------------------------------------------------------------------------- 1 | .Checkbox { 2 | margin-left: 4px; 3 | cursor: pointer; 4 | border: 1px solid gray; 5 | border-radius: 4px; 6 | padding: 3px 7px; 7 | background-color: rgba(250, 250, 250, 0.3); 8 | font-size: 0.7em; 9 | position: relative; 10 | top: -2px; 11 | } 12 | 13 | .Checkbox input { 14 | position: relative; 15 | top: 2px; 16 | left: -3px; 17 | } 18 | 19 | .Checkbox>span { 20 | white-space: nowrap; 21 | } 22 | 23 | .Checkbox:first-of-type { 24 | margin-left: 0px; 25 | } 26 | 27 | .checkbox-value { 28 | font-weight: bold; 29 | font-size: 1.1em; 30 | } 31 | 32 | .Checkbox.warning { 33 | border: 0px solid yellow; 34 | background-color: #585804; 35 | color: yellow; 36 | } 37 | 38 | .Checkbox.unknown { 39 | border: 0px solid orange; 40 | background-color: rgba(255, 177, 2, 0.3); 41 | color: orange; 42 | } 43 | 44 | .Checkbox.critical { 45 | border: 0px solid red; 46 | background-color: rgba(255, 2, 2, 0.3); 47 | color: orange; 48 | } 49 | 50 | .Checkbox.acked { 51 | border: 1px solid #004c00; 52 | background-color: #006700; 53 | color: lime; 54 | } 55 | 56 | .Checkbox.scheduled { 57 | border: 0px solid green; 58 | background-color: rgba(0, 253, 43, 0.3); 59 | } 60 | 61 | .Checkbox.flapping { 62 | border: 0px solid gray; 63 | background-color: rgba(255, 177, 2, 0.3); 64 | } 65 | 66 | .Checkbox.soft { 67 | border: 0px solid yellow; 68 | background-color: #585804; 69 | color: #fbdba0; 70 | } 71 | 72 | .Checkbox.notifications_disabled { 73 | border: 0px solid yellow; 74 | background-color: #484848; 75 | color: white; 76 | } 77 | 78 | /* host */ 79 | 80 | .Checkbox.down { 81 | border: 0px solid red; 82 | background-color: rgba(255, 2, 2, 0.3); 83 | color: orange; 84 | } 85 | 86 | .Checkbox.unreachable { 87 | border: 0px solid red; 88 | background-color: rgba(255, 2, 2, 0.3); 89 | color: orange; 90 | } 91 | 92 | .Checkbox.pending { 93 | border: 0px solid gray; 94 | background-color: rgba(250, 250, 250, 0.3); 95 | } 96 | 97 | /* dim */ 98 | 99 | .Checkbox.dim { 100 | border: 0px solid yellow; 101 | opacity: 0.5; 102 | } 103 | 104 | /* hidden checkbox */ 105 | 106 | .Checkbox.checkbox-hidden input[type=checkbox] { 107 | opacity: 0; 108 | display: none; 109 | } 110 | 111 | .Checkbox.checkbox-hidden.checkbox-unchecked { 112 | opacity: 0.5; 113 | } -------------------------------------------------------------------------------- /src/components/widgets/FilterCheckbox.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { ChangeEvent } from 'react'; 20 | import './FilterCheckbox.css'; 21 | 22 | interface FilterCheckboxProps { 23 | stateName: string; 24 | filterName: string; 25 | hideFilters: boolean; 26 | defaultChecked: boolean; 27 | howMany: number; 28 | howManyText: string; 29 | handleCheckboxChange: (e: ChangeEvent, propName: string, dataType: 'checkbox') => void; 30 | } 31 | 32 | const FilterCheckbox = ({ 33 | stateName, 34 | filterName, 35 | hideFilters, 36 | defaultChecked, 37 | howMany, 38 | howManyText, 39 | handleCheckboxChange, 40 | }: FilterCheckboxProps) => { 41 | 42 | const clicky = (e: React.MouseEvent) => { 43 | const f = e as unknown as ChangeEvent; 44 | handleCheckboxChange(f, stateName, 'checkbox'); 45 | }; 46 | 47 | let classN = 'Checkbox uppercase ' + filterName; 48 | //if (howMany) { classN += ' dim'; } 49 | if (hideFilters) { classN += ' checkbox-hidden'; } 50 | if (!defaultChecked) { classN += ' checkbox-unchecked'; } 51 | 52 | return ( 53 | 59 | ); 60 | 61 | }; 62 | 63 | export default FilterCheckbox; 64 | -------------------------------------------------------------------------------- /src/components/widgets/HistoryChart.css: -------------------------------------------------------------------------------- 1 | .HistoryChart { 2 | margin-left: -10px; 3 | margin-right: -5px; 4 | margin-bottom: -15px; 5 | } 6 | 7 | .history-chart-wrap { 8 | /* animation: fadeIn 1s; */ 9 | } 10 | 11 | @keyframes fadeIn { 12 | 0% { 13 | opacity: 0; 14 | height: 0px; 15 | } 16 | 17 | 100% { 18 | opacity: 1; 19 | height: 173px; 20 | } 21 | } -------------------------------------------------------------------------------- /src/components/widgets/HowMany.css: -------------------------------------------------------------------------------- 1 | .HowMany { 2 | display: inline-block; 3 | } 4 | 5 | .HowManyItem:first-of-type { 6 | margin-left: 0px; 7 | 8 | } 9 | 10 | .HowManyItem { 11 | /*border: 1px solid green;*/ 12 | background-color: lime; 13 | height: 8px; 14 | width: 8px; 15 | border-radius: 4px; 16 | display: inline-block; 17 | margin-left: 5px; 18 | } 19 | 20 | .HowManyItemProblem { 21 | /*border: 1px solid orange;*/ 22 | background-color: yellow; 23 | -webkit-animation: colorPulse 2s infinite alternate; 24 | } 25 | 26 | @-webkit-keyframes colorPulse { 27 | 0% { 28 | background-color: orange; 29 | } 30 | 31 | 100% { 32 | background-color: yellow; 33 | } 34 | } 35 | 36 | @media only screen and (max-device-width: 880px) { 37 | .HowManyItem { 38 | height: 4px; 39 | width: 4px; 40 | border-radius: 2px; 41 | } 42 | } 43 | 44 | @media only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3) { 45 | 46 | .HowManyItem { 47 | height: 4px; 48 | width: 4px; 49 | border-radius: 2px; 50 | } 51 | } -------------------------------------------------------------------------------- /src/components/widgets/HowMany.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Component } from 'react'; 20 | import './HowMany.css'; 21 | 22 | interface HowManyProps { 23 | howMany: number; 24 | howManyDown: number; 25 | } 26 | 27 | class HowMany extends Component { 28 | 29 | shouldComponentUpdate(nextProps: HowManyProps) { 30 | //console.log('shouldComponentUpdate', nextProps, nextState); 31 | if (nextProps.howMany !== this.props.howMany || nextProps.howManyDown !== this.props.howManyDown) { 32 | return true; 33 | } else { 34 | return false; 35 | } 36 | } 37 | 38 | render() { 39 | 40 | const howMany = this.props.howMany; 41 | const howManyDown = this.props.howManyDown; 42 | 43 | const res = [...Array(howMany)].map((_, i) => { 44 | if (i < howManyDown) { 45 | return ; 46 | } else { 47 | return ; 48 | } 49 | }); 50 | 51 | return ( 52 | <> 53 | {res} 54 | 55 | ); 56 | } 57 | } 58 | 59 | export default HowMany; 60 | -------------------------------------------------------------------------------- /src/components/widgets/HowManyEmoji.css: -------------------------------------------------------------------------------- 1 | .HowManyEmoji { 2 | display: inline-block; 3 | } 4 | 5 | .HowManyEmojiItem:first-of-type { 6 | margin-left: 0px; 7 | } 8 | 9 | .HowManyEmojiItem { 10 | display: inline-block; 11 | margin-left: 0px; 12 | } 13 | 14 | .HowManyEmojiItemProblem {} 15 | 16 | .HowManyEmojiWrap { 17 | margin-left: 10px; 18 | border: 0px solid yellow; 19 | font-size: 0.7em; 20 | } 21 | 22 | @media only screen and (max-device-width: 880px) { 23 | .HowManyEmojiItem { 24 | font-size: 0.5em; 25 | } 26 | } 27 | 28 | @media only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3) { 29 | 30 | .HowManyEmojiItem { 31 | font-size: 0.5em; 32 | } 33 | } -------------------------------------------------------------------------------- /src/components/widgets/HowManyEmoji.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Component } from 'react'; 20 | import './HowManyEmoji.css'; 21 | 22 | interface HowManyEmojiProps { 23 | howMany: number; 24 | howManyWarning: number; 25 | howManyCritical: number; 26 | } 27 | 28 | interface HowManyEmojiState { 29 | redEmoji: string; 30 | yellowEmoji: string; 31 | greenEmoji: string; 32 | } 33 | 34 | class HowManyEmoji extends Component { 35 | 36 | shouldComponentUpdate(nextProps: HowManyEmojiProps, nextState: HowManyEmojiState) { 37 | //console.log('HowManyEmoji shouldComponentUpdate', nextProps, nextState); 38 | if ( 39 | nextProps.howMany !== this.props.howMany || 40 | nextProps.howManyWarning !== this.props.howManyWarning || 41 | nextProps.howManyCritical !== this.props.howManyCritical || 42 | nextState.redEmoji !== this.state.redEmoji || 43 | nextState.yellowEmoji !== this.state.yellowEmoji || 44 | nextState.greenEmoji !== this.state.greenEmoji 45 | ) { 46 | return true; 47 | } else { 48 | return false; 49 | } 50 | } 51 | 52 | redEmojis = ['😡', '🌺', '💋', '🐙', '🌹', '🍉', '🍓', '🍟', '🎟', '🚒', '🥵', '🤬', '👹', '👺', '💄', '👠', '🐞', '🦑', '🦐', '🦞', '🦀']; 53 | yellowEmojis = ['😳', '😲', '🤯', '🥑', '💰', '🧽', '🔑', '⚠️', '🚸', '🔆', '🎗', '☹️', '😢', '🤮']; 54 | greenEmojis = ['🍀', '💚', '🥦', '🍏', '♻️', '🐢', '🐸', '🔋', '📗', '🌲', '🌴', '🥒', '🎾']; 55 | 56 | state = { 57 | redEmoji: '', 58 | yellowEmoji: '', 59 | greenEmoji: '' 60 | }; 61 | 62 | intervalHandle: NodeJS.Timeout | null = null; 63 | 64 | componentDidMount() { 65 | 66 | setTimeout(() => { 67 | this.selectEmojis(); 68 | }, 100); 69 | 70 | // Randomize the emojis on some interval 71 | //const interv = 60 * 60 * 1000; // hour 72 | const interv = 60 * 1000; // 60 seconds 73 | this.intervalHandle = setInterval(() => { 74 | this.selectEmojis(); 75 | }, interv); 76 | } 77 | 78 | componentWillUnmount() { 79 | if (this.intervalHandle) { 80 | clearInterval(this.intervalHandle); 81 | } 82 | } 83 | 84 | selectEmojis() { 85 | const redEmoji = this.redEmojis[Math.floor(Math.random() * this.redEmojis.length)]; 86 | const yellowEmoji = this.yellowEmojis[Math.floor(Math.random() * this.yellowEmojis.length)]; 87 | const greenEmoji = this.greenEmojis[Math.floor(Math.random() * this.greenEmojis.length)]; 88 | 89 | this.setState({ 90 | redEmoji, 91 | yellowEmoji, 92 | greenEmoji 93 | }) 94 | } 95 | 96 | render() { 97 | 98 | // add criticals 99 | const criticals = [...Array(this.props.howManyCritical)].map((item, i) => { 100 | return {this.state.redEmoji}; 101 | }); 102 | // add warnings 103 | const warnings = [...Array(this.props.howManyWarning)].map((item, i) => { 104 | return {this.state.yellowEmoji}; 105 | }); 106 | // merge two arrays 107 | const res = [...criticals, ...warnings]; 108 | 109 | return ( 110 | 111 | {res} 112 | 113 | ); 114 | } 115 | } 116 | 117 | export default HowManyEmoji; 118 | -------------------------------------------------------------------------------- /src/components/widgets/MiniMapCanvas.css: -------------------------------------------------------------------------------- 1 | .MiniMapCanvas { 2 | cursor: move; 3 | user-select: none; 4 | } 5 | 6 | .MiniMapCanvas img { 7 | width: calc(100% - 10px); 8 | margin-top: 5px; 9 | margin-left: 5px; 10 | margin-right: 5px; 11 | /* pointer-events: none; */ 12 | cursor: pointer; 13 | } 14 | 15 | .MiniMapCanvas .mmborder { 16 | border: 2px solid #ffff008a; 17 | background-color: rgba(255, 225, 0, 0.05); 18 | position: absolute; 19 | border-radius: 4px; 20 | left: 5px; 21 | right: 5px; 22 | } -------------------------------------------------------------------------------- /src/components/widgets/MiniMapWrap.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { Allotment } from "allotment"; 3 | import { debounce } from 'lodash'; 4 | import { useLocation } from "react-router-dom"; 5 | import MiniMapCanvas from '../widgets/MiniMapCanvas'; 6 | import { useAtom, useAtomValue } from 'jotai'; 7 | import { bigStateAtom, clientSettingsAtom } from 'atoms/settingsState'; 8 | import "allotment/dist/style.css"; 9 | import { saveLocalStorage } from 'helpers/nagiostv'; 10 | 11 | interface MiniMapWrapProps { 12 | children?: JSX.Element; 13 | } 14 | 15 | const MiniMapWrap = ({ children }: MiniMapWrapProps) => { 16 | 17 | const bigState = useAtomValue(bigStateAtom); 18 | const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom); 19 | 20 | const onResizeMiniMap = (e: number[]) => { 21 | //console.log('onResizeMiniMap', e); 22 | if (!bigState.isDoneLoading) { 23 | return undefined; 24 | } 25 | // Gets passed an array of [panel1width, panel2width] 26 | if (e && e.length === 2 && e[1] >= 0) { 27 | const newMiniMapWidth = Math.trunc(e[1]); 28 | if (newMiniMapWidth !== clientSettings.miniMapWidth) { 29 | 30 | setClientSettings(curr => { 31 | //console.log('setting miniMapWidth to', w); 32 | const o = { 33 | ...curr, 34 | miniMapWidth: newMiniMapWidth, 35 | }; 36 | saveLocalStorage('MiniMap', o); 37 | return o; 38 | }); 39 | } 40 | } 41 | return undefined; 42 | }; 43 | 44 | const debouncedResizeMiniMap = debounce((e: number[]) => onResizeMiniMap(e), 1000); 45 | 46 | // React router location 47 | const location = useLocation(); 48 | 49 | let whichElementToSnapshot = '.Dashboard'; 50 | if (location.pathname === '/settings') { 51 | whichElementToSnapshot = '.Settings'; 52 | } 53 | if (location.pathname === '/update') { 54 | whichElementToSnapshot = '.Update'; 55 | } 56 | if (location.pathname === '/help') { 57 | whichElementToSnapshot = '.Help'; 58 | } 59 | 60 | return ( 61 | <> 62 | {bigState.isDoneLoading && ( 63 | 66 | 67 | 68 | {children} 69 | 70 | 71 | 75 | 79 | 80 | 81 | 82 | )} 83 | 84 | ); 85 | }; 86 | 87 | export default MiniMapWrap; -------------------------------------------------------------------------------- /src/components/widgets/PollingSpinner.css: -------------------------------------------------------------------------------- 1 | .PollingSpinner.loading-spinner { 2 | position: absolute; 3 | right: -5px; 4 | top: 1px; 5 | 6 | color: rgb(138, 138, 45); 7 | margin-left: 15px; 8 | margin-right: 0px; 9 | font-size: 0.8em; 10 | } 11 | 12 | /* .two-column-container .PollingSpinner.loading-spinner { 13 | position: absolute; 14 | right: -5px; 15 | } */ 16 | 17 | .PollingSpinner.loading-spinner.loading-spinner-fadeout { 18 | color: #444; 19 | transition: color 1000ms ease-in; 20 | } 21 | 22 | .PollingSpinner svg { 23 | position: relative; 24 | top: -1px; 25 | left: -2px; 26 | } -------------------------------------------------------------------------------- /src/components/widgets/PollingSpinner.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | // State Management 3 | import { useAtom } from 'jotai'; 4 | import { clientSettingsAtom } from '../../atoms/settingsState'; 5 | // icons 6 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 7 | import { faSync, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; 8 | import './PollingSpinner.css'; 9 | 10 | interface PollingSpinnerProps { 11 | isFetching: boolean; 12 | isDemoMode: boolean; 13 | error: boolean; 14 | errorCount: number; 15 | //fetchFrequency: number; 16 | fetchVariableName: 'fetchAlertFrequency' | 'fetchHostFrequency' | 'fetchServiceFrequency'; 17 | } 18 | 19 | const PollingSpinner = ({ 20 | isFetching, 21 | isDemoMode, 22 | error, 23 | errorCount, 24 | //fetchFrequency, 25 | fetchVariableName, 26 | }: PollingSpinnerProps) => { 27 | //console.log('PollingSpinner run'); 28 | 29 | const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom); 30 | 31 | const fetchFrequency = clientSettings[fetchVariableName]; 32 | 33 | const onChangeSelect = (e: React.ChangeEvent) => { 34 | //console.log('onChangeSelect', e.target.value); 35 | //console.log('onChangeSelect', typeof e.target.value); 36 | 37 | setClientSettings(settings => { 38 | 39 | const newSettings = { 40 | ...settings, 41 | [fetchVariableName]: parseInt(e.target.value) 42 | }; 43 | 44 | console.log('Saving client settings', newSettings); 45 | 46 | localStorage.setItem('settings', JSON.stringify(newSettings)); // Save LocalStorage 47 | 48 | return newSettings; // Save state 49 | }); 50 | 51 | }; 52 | 53 | return ( 54 | 55 | {(!isDemoMode && error) && {errorCount} x   } 56 | 57 |   58 | 65 | 66 | ); 67 | }; 68 | 69 | // memoFn will re render the compopnent if return false 70 | function arePropsEqual(prev: PollingSpinnerProps, next: PollingSpinnerProps) { 71 | //console.log('arePropsEqual', prev, next); 72 | const equals = 73 | prev.isFetching === next.isFetching && 74 | prev.errorCount === next.errorCount; 75 | return equals; 76 | } 77 | 78 | export default React.memo(PollingSpinner, arePropsEqual); -------------------------------------------------------------------------------- /src/components/widgets/Progress.css: -------------------------------------------------------------------------------- 1 | .Progress { 2 | position: absolute; 3 | bottom: 1px; 4 | left: 0px; 5 | right: 0px; 6 | height: 8px; 7 | border-bottom-left-radius: 5px; 8 | border-bottom-right-radius: 5px; 9 | overflow: hidden; 10 | } 11 | 12 | @keyframes scaleup-keyframes { 13 | from { transform: scaleX(0); } 14 | to { transform: scaleX(1); } 15 | } 16 | 17 | @keyframes scaledown-keyframes { 18 | from { transform: scaleX(1); } 19 | to { transform: scaleX(0); } 20 | } 21 | 22 | .Progress .progress-bar { 23 | height: 8px; 24 | background-color: gray; 25 | width: 100%; 26 | opacity: 0.3; 27 | animation-timing-function: linear; 28 | transform-origin: left; 29 | } 30 | 31 | .Progress .progress-bar.color-yellow { 32 | background-color: yellow; 33 | } 34 | .Progress .progress-bar.color-orange { 35 | background-color: orange; 36 | } 37 | .Progress .progress-bar.color-red { 38 | background-color: pink; 39 | } 40 | .Progress .progress-bar.color-green { 41 | background-color: lime; 42 | } 43 | .Progress .progress-bar.color-gray { 44 | background-color: gray; 45 | } -------------------------------------------------------------------------------- /src/components/widgets/Progress.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | 20 | import { memo, useEffect, useRef, useState } from 'react'; 21 | import './Progress.css'; 22 | 23 | interface ProgressProps { 24 | next_check: number; 25 | color: string; 26 | } 27 | 28 | const Progress = ({ next_check, color }: ProgressProps) => { 29 | 30 | const [started, setStarted] = useState(false); 31 | const previous_next_check = useRef(0); 32 | 33 | useEffect(() => { 34 | 35 | // If next_check value increases, then store the value and set started to true 36 | if (next_check > previous_next_check.current) { 37 | previous_next_check.current = next_check; 38 | setStarted(true); 39 | } 40 | 41 | // Start a timer that will fire at the next_check time and set started to false 42 | const seconds = (next_check - Date.now()) / 1000; 43 | const timeoutHandle = setTimeout(() => { 44 | setStarted(false); 45 | }, seconds * 1000); 46 | return () => { 47 | clearTimeout(timeoutHandle); 48 | } 49 | 50 | }, [setStarted, next_check]); 51 | 52 | let seconds = (next_check - Date.now()) / 1000; 53 | if (seconds > 2) { 54 | seconds = seconds - 2; 55 | } 56 | if (seconds < 0) { 57 | seconds = 0; 58 | } 59 | 60 | // console.log('Progress render', next_check, Date.now(), seconds); 61 | 62 | const progressStyle = { 63 | animation: started ? 64 | `scaledown-keyframes 1s linear, scaleup-keyframes ${seconds}s linear` : 65 | 'none' 66 | }; 67 | 68 | return ( 69 |
70 |
71 |
72 | ); 73 | } 74 | 75 | // write a function to pass into memo 76 | function arePropsEqual(prevProps: ProgressProps, nextProps: ProgressProps) { 77 | // When this function returns, true we do not render, false we render 78 | return prevProps.next_check === nextProps.next_check; 79 | // return true; 80 | } 81 | 82 | export default memo(Progress, arePropsEqual); 83 | -------------------------------------------------------------------------------- /src/components/widgets/ScrollToTop.css: -------------------------------------------------------------------------------- 1 | .ScrollToTop { 2 | position: fixed; 3 | right: 20px; 4 | bottom: 20px; 5 | } 6 | 7 | .ScrollToTop button { 8 | font-size: 1.1em; 9 | border-radius: 5px; 10 | margin: 0 4px; 11 | color: #ddd; 12 | background-color: #333; 13 | border: 0px solid #ccc; 14 | } -------------------------------------------------------------------------------- /src/components/widgets/ScrollToTop.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import _ from 'lodash'; 3 | import './ScrollToTop.css'; 4 | 5 | const scrollAreaSelector = '.vertical-scroll-dash'; 6 | interface ScrollToTopProps { 7 | } 8 | interface ScrollToTopState { 9 | isAtBottom: boolean; 10 | } 11 | 12 | class ScrollToTop extends React.Component{ 13 | 14 | state = { 15 | isAtBottom: false 16 | }; 17 | 18 | debouncedScroll = _.debounce(() => this.handleScroll(), 500); 19 | 20 | componentDidMount() { 21 | const scrollDiv = document.querySelector(scrollAreaSelector); 22 | if (scrollDiv) { 23 | scrollDiv.addEventListener("scroll", this.debouncedScroll); 24 | } 25 | } 26 | 27 | componentWillUnmount() { 28 | const scrollDiv = document.querySelector(scrollAreaSelector); 29 | if (scrollDiv) { 30 | scrollDiv.removeEventListener("scroll", this.debouncedScroll); 31 | } 32 | } 33 | 34 | shouldComponentUpdate(nextProps: ScrollToTopProps, nextState: ScrollToTopState) { 35 | if (nextState.isAtBottom !== this.state.isAtBottom) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | handleScroll = () => { 42 | //console.log('handleScroll()'); 43 | const scrollDiv = document.querySelector(scrollAreaSelector) as HTMLElement; 44 | const dashboardDiv = document.querySelector('.Dashboard') as HTMLElement; 45 | 46 | if (!scrollDiv || !dashboardDiv) { 47 | return; 48 | } 49 | 50 | const windowHeight = "innerHeight" in window ? window.innerHeight : document.documentElement.offsetHeight; 51 | //const body = document.body; 52 | //const html = document.documentElement; 53 | //const docHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); 54 | const divHeight = Math.max(dashboardDiv.clientHeight, dashboardDiv.offsetHeight); 55 | const windowBottom = windowHeight + scrollDiv.scrollTop; 56 | // console.log('dashboardDiv.scrollTop', dashboardDiv.scrollTop); 57 | // console.log('dashboardDiv height', dashboardDiv.clientHeight, dashboardDiv.offsetHeight); 58 | // console.log('windowBottom', windowBottom); 59 | // console.log('divHeight', divHeight); 60 | const atBottom = windowBottom >= divHeight + 80; 61 | 62 | // Prevent state updates if the value is the same 63 | if (atBottom !== this.state.isAtBottom) { 64 | if (atBottom) { 65 | this.setState({ 66 | isAtBottom: true 67 | }); 68 | } else { 69 | this.setState({ 70 | isAtBottom: false 71 | }); 72 | } 73 | } 74 | }; 75 | 76 | scrollUp = () => { 77 | const scrollDiv = document.querySelector(scrollAreaSelector); 78 | if (scrollDiv) { 79 | scrollDiv.scrollTo({ top: 0, behavior: 'smooth' }); 80 | } 81 | }; 82 | 83 | render() { 84 | return ( 85 |
86 | {this.state.isAtBottom && } 87 |
88 | ); 89 | } 90 | } 91 | 92 | export default ScrollToTop; -------------------------------------------------------------------------------- /src/helpers/audio.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // so this new Audio() function dumps a stacktrace when it cant find the audio file to play 20 | // we should add some extra test that the file exists, if possible 21 | /* 22 | * Chrome autoplay policy 23 | * https://developers.google.com/web/updates/2017/09/autoplay-policy-changes 24 | */ 25 | import { debounce } from 'lodash'; 26 | import { ClientSettings } from 'types/settings'; 27 | 28 | // debounce the playSoundEffect function so multiple sounds at the same time wont freak out the audio engine 29 | export const playSoundEffectDebounced = debounce(function (type, state, settings) { 30 | playSoundEffect(type, state, settings); 31 | }, 200); 32 | 33 | // if sound is delimited by a semicolon; then choose one at random 34 | function pickSound(soundConfig: string) { 35 | if (soundConfig.indexOf(';') !== -1) { 36 | const soundArray = soundConfig.split(';').filter(sound => sound.length > 0); 37 | return soundArray[Math.floor(Math.random() * soundArray.length)]; 38 | } 39 | return soundConfig; 40 | } 41 | 42 | // playSoundEffect 43 | export function playSoundEffect(type: string, state: string, settings: ClientSettings) { 44 | 45 | const audioCritical = pickSound(settings.soundEffectCritical); 46 | const audioWarning = pickSound(settings.soundEffectWarning); 47 | const audioOk = pickSound(settings.soundEffectOk); 48 | 49 | //console.log('playSoundEffect', type, state, audioCritical, audioWarning, audioOk); 50 | 51 | let audio; 52 | 53 | switch (type + state) { 54 | case 'hostdown': 55 | case 'hostunreachable': 56 | audio = new Audio(audioCritical); 57 | break; 58 | case 'hostup': 59 | audio = new Audio(audioOk); 60 | break; 61 | case 'servicecritical': 62 | audio = new Audio(audioCritical); 63 | break; 64 | case 'servicewarning': 65 | audio = new Audio(audioWarning); 66 | break; 67 | case 'serviceok': 68 | audio = new Audio(audioOk); 69 | break; 70 | default: 71 | break; 72 | } 73 | 74 | if (audio) { 75 | const promise = audio.play(); 76 | promise.catch((err) => { 77 | if (err instanceof DOMException) { 78 | console.log(err.message); 79 | //console.log(err.code); 80 | //console.log(err.name); 81 | 82 | // TODO: pop up a message to the UI. Not just console.log here 83 | console.log('Blocked by autoplay prevention. Touch the UI to enable sound'); 84 | } else { 85 | console.log('error'); 86 | console.log(err); 87 | console.log(typeof err); 88 | } 89 | }); 90 | } 91 | } 92 | 93 | function massageSpeakingWords(words: string) { 94 | // Convert NEMS uppercase to lowercase which speaks it better 95 | const newWords = words.replace('NEMS', 'nems'); 96 | return newWords; 97 | } 98 | 99 | export function speakAudio(words: string, voice: string) { 100 | 101 | //console.log('speakAudio', words, voice); 102 | const massagedWords = massageSpeakingWords(words); 103 | 104 | let sayWhat; 105 | try { 106 | sayWhat = new SpeechSynthesisUtterance(massagedWords); 107 | } catch (e) { 108 | console.log('SpeechSynthesisUtterance not supported on this browser'); 109 | return; 110 | } 111 | 112 | // test for window.speechSynthesis 113 | if (!window.speechSynthesis) { 114 | console.log('speechSynthesis not supported on this browser'); 115 | return; 116 | } 117 | 118 | if (voice) { 119 | let mySpeechSynthesisVoice = window.speechSynthesis.getVoices().filter(v => v.name === voice); 120 | if (mySpeechSynthesisVoice.length > 0) { 121 | sayWhat.voice = mySpeechSynthesisVoice[0]; 122 | } 123 | } 124 | window.speechSynthesis.speak(sayWhat); 125 | 126 | } 127 | -------------------------------------------------------------------------------- /src/helpers/axios.ts: -------------------------------------------------------------------------------- 1 | import { AxiosError } from "axios"; 2 | import { SetStateAction } from "jotai"; 3 | 4 | export const handleFetchFail = (setFn: SetStateAction, error: AxiosError, url: string, incrementErrorCount: boolean) => { 5 | // console.log('handleFetchFail DEBUG error', error); 6 | 7 | let errorMessage = ''; 8 | if (error?.code === 'ERR_NETWORK') { 9 | errorMessage = `ERROR: ${error.code} CONNECTION REFUSED ${error.message} ${url}`; 10 | } else if (error && error.code && error.message) { 11 | errorMessage = `ERROR: ${error.code} ${error.message} ${url}`; 12 | } else { 13 | errorMessage = `UNKNOWN ERROR to ${url} check console`; 14 | console.log('Axios unknown error', error); 15 | } 16 | 17 | if (incrementErrorCount) { 18 | setFn((curr: any) => ({ 19 | ...curr, 20 | error: true, 21 | errorCount: curr.errorCount + 1, 22 | errorMessage 23 | })); 24 | } else { 25 | setFn((curr: any) => ({ 26 | ...curr, 27 | error: true, 28 | errorMessage 29 | })); 30 | } 31 | }; 32 | 33 | -------------------------------------------------------------------------------- /src/helpers/colors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export function hostBorderClass(status) { 20 | //const status = this.get('servicedetail.status'); 21 | let classString = ''; 22 | 23 | switch (status) { 24 | case 1: 25 | classString = 'border-gray'; // PENDING 26 | break; 27 | case 2: 28 | classString = 'border-green'; // UP 29 | break; 30 | case 4: 31 | classString = 'border-red'; // DOWN 32 | break; 33 | case 8: 34 | classString = 'border-orange'; // UNREACHABLE 35 | break; 36 | default: 37 | classString = ''; 38 | break; 39 | } 40 | return classString; 41 | } 42 | 43 | export function hostTextClass(status) { 44 | let classString = ''; 45 | 46 | switch (status) { 47 | case 1: 48 | classString = 'color-gray'; // PENDING 49 | break; 50 | case 2: 51 | classString = 'color-green'; // UP 52 | break; 53 | case 4: 54 | classString = 'color-red'; // DOWN 55 | break; 56 | case 8: 57 | classString = 'color-orange'; // UNREACHABLE 58 | break; 59 | default: 60 | classString = ''; 61 | break; 62 | } 63 | return classString; 64 | } 65 | 66 | export function serviceBorderClass(status) { 67 | //const status = this.get('servicedetail.status'); 68 | let classString = ''; 69 | 70 | switch (status) { 71 | case 1: 72 | classString = 'border-gray'; // PENDING 73 | break; 74 | case 2: 75 | classString = 'border-green'; // OK 76 | break; 77 | case 4: 78 | classString = 'border-yellow'; // WARNING 79 | break; 80 | case 8: 81 | classString = 'border-orange'; // UNKNOWN 82 | break; 83 | case 16: 84 | classString = 'border-red'; // CRITICAL 85 | break; 86 | default: 87 | classString = ''; 88 | break; 89 | } 90 | return classString; 91 | } 92 | 93 | export function serviceTextClass(status) { 94 | let classString = ''; 95 | 96 | switch (status) { 97 | case 1: 98 | classString = 'color-gray'; // pending 99 | break; 100 | case 2: 101 | classString = 'color-green'; // ok 102 | break; 103 | case 4: 104 | classString = 'color-yellow'; // warning 105 | break; 106 | case 8: 107 | classString = 'color-orange'; // unknown 108 | break; 109 | case 16: 110 | classString = 'color-red'; // critical 111 | break; 112 | default: 113 | classString = ''; 114 | break; 115 | } 116 | return classString; 117 | } 118 | 119 | 120 | export function alertBorderClass(object_type, state) { 121 | let classString = ''; 122 | 123 | switch (state) { 124 | case 1: 125 | classString = 'border-green'; // HOST OK 126 | break; 127 | case 2: 128 | classString = 'border-red'; // HOST DOWN 129 | break; 130 | case 4: 131 | classString = 'border-orange'; // HOST UNREACHABLE 132 | break; 133 | case 8: 134 | classString = 'border-green'; // SERVICE OK 135 | break; 136 | case 16: 137 | classString = 'border-yellow'; // SERVICE WARNING 138 | break; 139 | case 32: 140 | classString = 'border-red'; // SERVICE CRITICAL 141 | break; 142 | case 64: 143 | classString = 'border-orange'; // SERVICE UNKNOWN 144 | break; 145 | default: 146 | classString = ''; 147 | break; 148 | } 149 | return classString; 150 | } 151 | 152 | export function alertTextClass(object_type, state) { 153 | let classString = ''; 154 | 155 | switch (state) { 156 | case 1: 157 | classString = 'color-green'; // HOST UP 158 | break; 159 | case 2: 160 | classString = 'color-red'; // HOST DOWN 161 | break; 162 | case 4: 163 | classString = 'color-orange'; // HOST UNREACHABKE 164 | break; 165 | case 8: 166 | classString = 'color-green'; // SERVICE OK 167 | break; 168 | case 16: 169 | classString = 'color-yellow'; // SERVICE WARNING 170 | break; 171 | case 32: 172 | classString = 'color-red'; // SERVICE CRITICAL 173 | break; 174 | case 64: 175 | classString = 'color-orange'; // SERVICE UNKNOWN 176 | break; 177 | default: 178 | classString = ''; 179 | break; 180 | } 181 | return classString; 182 | } -------------------------------------------------------------------------------- /src/helpers/date-math.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | export function ifQuietFor(nowtime, prevtime, minutes) { 20 | let diff = prevtime - nowtime; 21 | if (diff > minutes * 60 * 1000) { 22 | return true; 23 | } else { 24 | return false; 25 | } 26 | } -------------------------------------------------------------------------------- /src/helpers/language.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | /* 20 | * Languages! 21 | * To add a new language, add a new file in the languages folder 22 | * following the same pattern of an existing file there. 23 | * Then add the language to import and languages array and translate 24 | * function in this file. That's it! 25 | */ 26 | 27 | import { phrases as spanish } from './languages/spanish'; 28 | import { phrases as french } from './languages/french'; 29 | 30 | export const languages = [ 31 | { name: "English", code: "en" }, 32 | { name: "Spanish", code: "es" }, 33 | { name: "French", code: "fr" } 34 | ]; 35 | 36 | export function translate(phrase, language) { 37 | let word; 38 | switch(language) { 39 | case 'Spanish': 40 | word = spanish[phrase]; 41 | break; 42 | case 'French': 43 | word = french[phrase]; 44 | break; 45 | default: 46 | word = phrase; 47 | break; 48 | } 49 | if (typeof word === 'undefined') { 50 | console.log(`Word [${phrase}] not found in [${language}] language pack.`); 51 | } 52 | return word; 53 | } -------------------------------------------------------------------------------- /src/helpers/languages/french.ts: -------------------------------------------------------------------------------- 1 | export const phrases = { 2 | 3 | 'host': 'hôte', 4 | 'hosts': 'hôtes', 5 | 'service': 'service', 6 | 'services': 'services', 7 | 8 | 'pending': 'en attente', 9 | 'up': 'en haut', 10 | 'down': 'vers le bas', 11 | 'unreachable': 'inaccessible', 12 | 'acked': 'reconnaître', 13 | 'scheduled': 'prévu', 14 | 'flapping': 'battement', 15 | 'ok': 'bien', 16 | 'warning': 'attention', 17 | 'critical': 'critique', 18 | 'unknown': 'inconnu', 19 | 20 | 'host up': 'hôte en haut', 21 | 'host down': 'hôte vers le bas', 22 | 'host unreachable': 'hôte inaccessible', 23 | 'service ok': 'service en haut', 24 | 'service warning': 'service attention', 25 | 'service critical': 'service critique', 26 | 'service unknown': 'service inconnu', 27 | 28 | 'soft': 'doux', 29 | 'hard': 'fort', 30 | 31 | 'Last OK': 'Dernière fois bon', 32 | 'Last UP': 'Dernière fois en haut', 33 | 'Last check was': 'Le dernier chèque était', 34 | 'Next check in': 'Prochain enregistrement dans', 35 | 'ago': '', 36 | 37 | 'All': 'Tout', 38 | 'hosts are UP': 'hôtes sont bons', 39 | 'services are OK': 'services sont bons', 40 | 41 | 'history': 'l\'histoire', 42 | 'alerts in the past': 'alertes dans le passé', 43 | 'trimming at': 'couper à', 44 | 45 | 'quiet': 'calme', 46 | 47 | 'on': 'sur', 48 | 'is': 'est', 49 | 'and': 'et', 50 | 51 | 'show filters': 'afficher les filtres', 52 | 'last update': 'dernière mise à jour', 53 | 'settings': 'réglages', 54 | 55 | 'newest first': 'le plus récent d\'abord', 56 | 'oldest first': 'le plus ancien en premier', 57 | 58 | "show more": "montre plus", 59 | "show less": "montre moins", 60 | 61 | "day": "journée", 62 | "days": "journées", 63 | "hour": "heure", 64 | "hours": "heures", 65 | "minute": "minute", 66 | "minutes": "minutes", 67 | "second": "second", 68 | "seconds": "seconds" 69 | 70 | }; -------------------------------------------------------------------------------- /src/helpers/languages/spanish.ts: -------------------------------------------------------------------------------- 1 | export const phrases = { 2 | 3 | 'host': 'computadora', 4 | 'hosts': 'computadoras', 5 | 'service': 'servicio', 6 | 'services': 'servicios', 7 | 8 | 'pending': 'pendiente', 9 | 'up': 'arriba', 10 | 'down': 'abajo', 11 | 'unreachable': 'inalcanzable', 12 | 'acked': 'admitido', 13 | 'scheduled': 'programado', 14 | 'flapping': 'agitarse', 15 | 'ok': 'bueno', 16 | 'warning': 'aviso', 17 | 'critical': 'crítico', 18 | 'unknown': 'desconocida', 19 | 20 | 'host up': 'computadora arriba', 21 | 'host down': 'computadora abajo', 22 | 'host unreachable': 'computadora inalcanzable', 23 | 'service ok': 'servicio bueno', 24 | 'service warning': 'servicio aviso', 25 | 'service critical': 'servicio crítico', 26 | 'service unknown': 'servicio desconocida', 27 | 28 | 'soft': 'mullido', 29 | 'hard': 'fuerte', 30 | 31 | 'Last OK': 'El último tiempo bueno hace', 32 | 'Last UP': 'El último tiempo bueno hace', 33 | 'Last check was': 'La última comprobé fue', 34 | 'Next check in': 'Siguiente comprobé en', 35 | 'ago': '', 36 | 37 | 'All': 'Todas las', 38 | 'hosts are UP': 'computadoras son buenas', 39 | 'services are OK': 'servicios son buenas', 40 | 41 | 'history': 'historia', 42 | 'alerts in the past': 'alarmas en el anterior', 43 | 'trimming at': 'cortante en el', 44 | 45 | 'quiet': 'tranquilo', 46 | 47 | 'on': 'en', 48 | 'is': 'es', 49 | 'and': 'y', 50 | 51 | 'show filters': 'mostrar filtros', 52 | 'last update': 'última actualización', 53 | 'settings': 'ajustes', 54 | 55 | 'newest first': 'los más reciente primero', 56 | 'oldest first': 'los más viejos primero', 57 | 58 | "show more": "mostrar más", 59 | "show less": "mostrar menos", 60 | 61 | "day": "dia", 62 | "days": "dias", 63 | "hour": "hora", 64 | "hours": "horas", 65 | "minute": "minuto", 66 | "minutes": "minutos", 67 | "second": "segundo", 68 | "seconds": "segundos" 69 | 70 | }; -------------------------------------------------------------------------------- /src/helpers/nagios.ts: -------------------------------------------------------------------------------- 1 | /*eslint no-unreachable: "off"*/ 2 | 3 | /** 4 | * NagiosTV https://nagiostv.com 5 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 6 | * 7 | * This program is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU General Public License as published by 9 | * the Free Software Foundation, either version 2 of the License, or 10 | * (at your option) any later version. 11 | * 12 | * This program is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | * GNU General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU General Public License 18 | * along with this program. If not, see . 19 | */ 20 | 21 | export function nagiosHostStatus(status: number) { 22 | switch (status) { 23 | case 1: 24 | return 'pending'; 25 | case 2: 26 | return 'up'; 27 | case 4: 28 | return 'down'; 29 | case 8: 30 | return 'unreachable'; 31 | default: 32 | return 'Unknown host status ' + status; 33 | } 34 | return 'Unknown host status ' + status; 35 | } 36 | 37 | export function nagiosStateType(state_type: number) { 38 | switch (state_type) { 39 | case 0: 40 | return 'soft'; 41 | case 1: 42 | return 'hard'; 43 | default: 44 | return 'Unknown state_type ' + state_type; 45 | } 46 | return 'Unknown state_type ' + state_type; 47 | } 48 | 49 | export function nagiosServiceStatus(status: number) { 50 | switch (status) { 51 | case 1: 52 | return 'pending'; 53 | case 2: 54 | return 'ok'; 55 | case 4: 56 | return 'warning'; 57 | case 8: 58 | return 'unknown'; 59 | case 16: 60 | return 'critical'; 61 | default: 62 | return 'Unknown service status ' + status; 63 | } 64 | return 'Unknown service status ' + status; 65 | } 66 | 67 | export function nagiosAlertState(state: number) { 68 | switch (state) { 69 | case 1: 70 | return 'host up'; 71 | case 2: 72 | return 'host down'; 73 | case 4: 74 | return 'host unreachable'; 75 | case 8: 76 | return 'service ok'; 77 | case 16: 78 | return 'service warning'; 79 | case 32: 80 | return 'service critical'; 81 | case 64: 82 | return 'service unknown'; 83 | default: 84 | return 'Unknown alert state ' + state; 85 | } 86 | return 'Unknown alert state ' + state; 87 | } 88 | 89 | export function nagiosAlertStateType(state_type: number) { 90 | switch (state_type) { 91 | case 1: 92 | return 'hard'; 93 | case 2: 94 | return 'soft'; 95 | default: 96 | return 'Unknown state_type ' + state_type; 97 | } 98 | return 'Unknown alert state_type ' + state_type; 99 | } -------------------------------------------------------------------------------- /src/helpers/nagiostv.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | import { Host, HostList, Service, ServiceList } from "types/hostAndServiceTypes"; 20 | import { ClientSettings } from "types/settings"; 21 | import Cookie from 'js-cookie'; 22 | 23 | export function cleanDemoDataHostlist(hostlist: HostList) { 24 | //console.log(hostlist); 25 | Object.keys(hostlist).forEach(key => { 26 | //if (hostlist[key].status === 2) { 27 | hostlist[key].status = 2; 28 | hostlist[key].last_time_up = new Date().getTime(); 29 | hostlist[key].is_flapping = false; 30 | hostlist[key].problem_has_been_acknowledged = false; 31 | hostlist[key].scheduled_downtime_depth = 0; 32 | return false; 33 | //} 34 | }); 35 | return hostlist; 36 | } 37 | export function cleanDemoDataServicelist(servicelist: ServiceList) { 38 | Object.keys(servicelist).forEach(hostkey => { 39 | Object.keys(servicelist[hostkey]).forEach(key => { 40 | //if (servicelist[hostkey][key].status === 2) { 41 | servicelist[hostkey][key].status = 2; 42 | servicelist[hostkey][key].last_time_ok = new Date().getTime(); 43 | servicelist[hostkey][key].is_flapping = false; 44 | servicelist[hostkey][key].problem_has_been_acknowledged = false; 45 | servicelist[hostkey][key].scheduled_downtime_depth = 0; 46 | return false; 47 | //} 48 | }); 49 | return false; 50 | }); 51 | return servicelist; 52 | } 53 | 54 | export function convertHostObjectToArray(hostlist: Record) { 55 | let hostProblemsArray: Host[] = []; 56 | 57 | if (hostlist) { 58 | Object.keys(hostlist).forEach((k) => { 59 | // if host status is NOT UP 60 | // or host is flapping, 61 | // or host is scheduled downtime 62 | // we add it to the array 63 | if (hostlist[k].status !== 2 || hostlist[k].is_flapping || hostlist[k].scheduled_downtime_depth > 0) { 64 | hostProblemsArray.push(hostlist[k]); 65 | } 66 | }); 67 | } 68 | 69 | return hostProblemsArray; 70 | } 71 | 72 | export function convertServiceObjectToArray(servicelist: Record>) { 73 | let serviceProblemsArray: Service[] = []; 74 | 75 | if (servicelist) { 76 | Object.keys(servicelist).forEach((k) => { 77 | Object.keys(servicelist[k]).forEach((l) => { 78 | // if service status is NOT OK 79 | // or service is flapping, 80 | // or host is scheduled downtime 81 | // we add it to the array 82 | if (servicelist[k][l].status !== 2 || 83 | servicelist[k][l].is_flapping || 84 | servicelist[k][l].scheduled_downtime_depth > 0) { 85 | // add it to the array of service problems 86 | serviceProblemsArray.push(servicelist[k][l]); 87 | } 88 | }); 89 | }); 90 | } 91 | 92 | return serviceProblemsArray; 93 | } 94 | 95 | 96 | // Utility functions to handle localStorage and cookies 97 | export const isLocalStorageEnabled = (): boolean => { 98 | try { 99 | const testKey = 'test'; 100 | localStorage.setItem(testKey, 'testValue'); 101 | localStorage.removeItem(testKey); 102 | return true; 103 | } catch (e) { 104 | return false; 105 | } 106 | }; 107 | 108 | export const doesLocalStorageSettingsExist = (): boolean => { 109 | if (isLocalStorageEnabled()) { 110 | return localStorage.getItem('settings') !== null; 111 | } else { 112 | return false; 113 | } 114 | } 115 | 116 | export const saveCookie = (changeString: string, obj: ClientSettings) => { 117 | Cookie.set('settings', JSON.stringify(obj)); 118 | console.log('Saved cookie', changeString, obj); 119 | } 120 | 121 | export const saveLocalStorage = (changeString: string, obj: ClientSettings) => { 122 | if (isLocalStorageEnabled()) { 123 | localStorage.setItem('settings', JSON.stringify(obj)); 124 | console.log('Saved localStorage', changeString, obj); 125 | } else { 126 | console.error('LocalStorage is not enabled. Trying to save to cookie instead.'); 127 | saveCookie(changeString, obj); 128 | } 129 | }; 130 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | padding: 0; 22 | font-family: sans-serif; 23 | background-color: rgb(17, 18, 23); 24 | color: #ddd; 25 | font-size: 1.0em; 26 | } 27 | 28 | @media only screen and (max-device-width: 480px) { 29 | body { 30 | font-size: 0.7em !important; 31 | } 32 | } 33 | 34 | @media only screen and (device-width : 375px) and (device-height : 812px) and (-webkit-device-pixel-ratio : 3) { 35 | 36 | body { 37 | font-size: 0.7em !important; 38 | } 39 | } 40 | 41 | /* width */ 42 | ::-webkit-scrollbar { 43 | width: 7px; 44 | } 45 | 46 | /* Track */ 47 | ::-webkit-scrollbar-track { 48 | background: #222; 49 | } 50 | 51 | /* Handle */ 52 | ::-webkit-scrollbar-thumb { 53 | background: #333; 54 | } 55 | 56 | /* Handle on hover */ 57 | ::-webkit-scrollbar-thumb:hover { 58 | background: #555; 59 | } -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * NagiosTV https://nagiostv.com 3 | * Copyright (C) 2008-2025 Chris Carey https://chriscarey.com 4 | * 5 | * This program is free software: you can redistribute it and/or modify 6 | * it under the terms of the GNU General Public License as published by 7 | * the Free Software Foundation, either version 2 of the License, or 8 | * (at your option) any later version. 9 | * 10 | * This program is distributed in the hope that it will be useful, 11 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | * GNU General Public License for more details. 14 | * 15 | * You should have received a copy of the GNU General Public License 16 | * along with this program. If not, see . 17 | */ 18 | 19 | // Fix for IE11 - This must be the first line in src/index.js 20 | import 'react-app-polyfill/ie11'; 21 | import 'react-app-polyfill/stable'; 22 | 23 | import { createRoot } from 'react-dom/client'; 24 | import './index.css'; 25 | import App from './App'; 26 | //import registerServiceWorker from './registerServiceWorker'; 27 | 28 | // Delete caches caused by the registerServiceWorker. 29 | // TODO: remove this from the project after a few versions. 30 | // Just to help clear out any client side cache that's hard as heck to clear. 31 | // Clear cache in the browser does not do it. 32 | 33 | // This is actually causing a crash on Windows (Cant remember Chrome or FF) 34 | // With a security error message (TODO: repro and write it down here) 35 | // The line that crashes is the caches.keys(), even though it's in a try catch!? 36 | 37 | // try { 38 | // caches.keys().then(function(names) { 39 | // for (let name of names) 40 | // caches.delete(name); 41 | // }); 42 | // } catch (e) { 43 | // console.log('Had a problem clearing the serviceWorker cache.'); 44 | // } 45 | 46 | // React 17 47 | // ReactDOM.render(, document.getElementById('root')); 48 | 49 | // React 18 50 | const container = document.getElementById('app'); 51 | /* If there is no container, display an error message to the DOM */ 52 | if (!container) { 53 | const error = document.createElement('div'); 54 | error.innerHTML = 'Error: Could not find the root element. Make sure the element with id="app" is in the DOM.'; 55 | document.body.appendChild(error); 56 | throw new Error('Could not find the root element. Make sure the element with id="app" is in the DOM.'); 57 | } 58 | const root = createRoot(container); // createRoot(container!) if you use TypeScript 59 | // root.render(); 60 | root.render(); 61 | 62 | //registerServiceWorker(); 63 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/settingsLoader.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const SettingsLoader = () => { 4 | return ( 5 |
6 | SettingsLoader 7 |
8 | ); 9 | }; 10 | 11 | export default SettingsLoader; 12 | -------------------------------------------------------------------------------- /src/types/commentTypes.ts: -------------------------------------------------------------------------------- 1 | export interface Comment { 2 | author: string; 3 | entry_time: number; 4 | comment_data: string; 5 | } 6 | 7 | // The structure of the data coming back from the server 8 | export interface CommentListResponseObject { 9 | host_name: string; 10 | service_description: string; 11 | comment_type: number; 12 | author: string; 13 | entry_time: number; 14 | comment_data: string; 15 | 16 | // There are a LOT more fields but I'm only typing the ones I need for now 17 | } 18 | 19 | export interface CommentListObject { 20 | hosts: Record; 21 | services: Record; 22 | } 23 | -------------------------------------------------------------------------------- /src/types/hostAndServiceTypes.ts: -------------------------------------------------------------------------------- 1 | export type HostList = { 2 | [hostname: string]: Host 3 | }; 4 | 5 | export interface Host { 6 | // TODO: sort these 7 | name: string; 8 | last_time_up: number; 9 | status: number; 10 | is_flapping: boolean; 11 | problem_has_been_acknowledged: boolean; 12 | scheduled_downtime_depth: number; 13 | state_type: number; 14 | next_check: number; 15 | last_check: number; 16 | check_type: number; // Active/Passive 17 | notifications_enabled: boolean; 18 | current_attempt: number; 19 | max_attempts: number; 20 | plugin_output: string; 21 | checks_enabled: boolean; 22 | } 23 | 24 | export interface HostWrap { 25 | error: boolean; 26 | errorCount: number; 27 | errorMessage: string; 28 | lastUpdate: number; 29 | response: Record; 30 | problemsArray: Host[] 31 | } 32 | 33 | export type ServiceList = { 34 | [hostname: string]: { 35 | [servicename: string]: Service; 36 | }; 37 | }; 38 | 39 | export interface Service { 40 | // TODO: sort these 41 | host_name: string; 42 | description: string; 43 | last_time_ok: number; 44 | problem_has_been_acknowledged: boolean; 45 | scheduled_downtime_depth: number; 46 | status: number; 47 | is_flapping: boolean; 48 | state_type: number; 49 | notifications_enabled: boolean; 50 | next_check: number; 51 | last_check: number; 52 | check_type: number; 53 | current_attempt: number; 54 | max_attempts: number; 55 | plugin_output: string; 56 | checks_enabled: boolean; 57 | } 58 | 59 | export interface ServiceWrap { 60 | error: boolean; 61 | errorCount: number; 62 | errorMessage: string; 63 | lastUpdate: number; 64 | response: Record; 65 | problemsArray: Service[] 66 | } 67 | 68 | export interface Alert { 69 | name: string; 70 | host_name: string; 71 | timestamp: number; 72 | state: number; 73 | state_type: number; 74 | description: string; 75 | plugin_output: string; 76 | object_type: number; 77 | } 78 | export interface AlertWrap { 79 | error: boolean; 80 | errorCount: number; 81 | errorMessage: string; 82 | lastUpdate: number; 83 | response: Record; 84 | responseArray: Alert[] 85 | } 86 | -------------------------------------------------------------------------------- /src/types/settings.ts: -------------------------------------------------------------------------------- 1 | export interface BigState { 2 | currentVersion: number; // This gets incremented with each new release (manually) 3 | currentVersionString: string; // This gets incremented with each new release (manually) 4 | latestVersion: number; 5 | latestVersionString: string; 6 | lastVersionCheckTime: number; 7 | 8 | isDemoMode: boolean; 9 | isDebugMode: boolean; 10 | isStressTestMode: boolean; 11 | useFakeSampleData: boolean; 12 | 13 | isRemoteSettingsLoaded: boolean; 14 | isLocalSettingsLoaded: boolean; // I have this to render things only after localSettings is loaded 15 | isDoneLoading: boolean; 16 | 17 | hideFilters: boolean; 18 | isLeftPanelOpen: boolean; 19 | } 20 | 21 | export interface ClientSettings { 22 | 23 | titleString: string; 24 | dataSource: string; 25 | baseUrl: string; // Base path to Nagios cgi-bin folder 26 | livestatusPath: string; 27 | 28 | fetchHostFrequency: number; // seconds 29 | fetchServiceFrequency: number; // seconds 30 | fetchAlertFrequency: number; // seconds 31 | fetchHostGroupFrequency: number; // seconds 32 | fetchCommentFrequency: number; // seconds 33 | 34 | alertDaysBack: number; 35 | alertHoursBack: number; 36 | alertMaxItems: number; 37 | 38 | hostsAndServicesSideBySide: boolean; 39 | hideSummarySection: boolean; 40 | hideMostRecentAlertSection: boolean; 41 | hideServiceSection: boolean; 42 | hideServicePending: boolean; 43 | hideServiceWarning: boolean; 44 | hideServiceUnknown: boolean; 45 | hideServiceCritical: boolean; 46 | hideServiceAcked: boolean; 47 | hideServiceScheduled: boolean; 48 | hideServiceFlapping: boolean; 49 | hideServiceSoft: boolean; 50 | hideServiceNotificationsDisabled: boolean; 51 | serviceSortOrder: string; 52 | 53 | hideHostSection: boolean; 54 | hideHostPending: boolean; 55 | hideHostDown: boolean; 56 | hideHostUnreachable: boolean; 57 | hideHostAcked: boolean; 58 | hideHostScheduled: boolean; 59 | hideHostFlapping: boolean; 60 | hideHostSoft: boolean; 61 | hideHostNotificationsDisabled: boolean; 62 | hostSortOrder: string; 63 | 64 | hideHistory: boolean; 65 | hideHistoryTitle: boolean; 66 | hideHistory24hChart: boolean; 67 | hideHistoryChart: boolean; 68 | 69 | hideAlertSoft: boolean; 70 | 71 | hostgroupFilter: string; 72 | servicegroupFilter: string; 73 | 74 | versionCheckDays: number; 75 | 76 | language: string; 77 | locale: string; 78 | dateFormat: string; 79 | clockDateFormat: string; 80 | clockTimeFormat: string; 81 | 82 | // audio and visual 83 | fontSizeEm: string; 84 | customLogoEnabled: boolean; 85 | customLogoUrl: string; 86 | 87 | doomguyEnabled: boolean; 88 | doomguyConcernedAt: number; 89 | doomguyAngryAt: number; 90 | doomguyBloodyAt: number; 91 | 92 | showEmoji: boolean; 93 | speakItems: boolean; 94 | speakItemsVoice: string; 95 | playSoundEffects: boolean; 96 | soundEffectCritical: string; 97 | soundEffectWarning: string; 98 | soundEffectOk: string; 99 | showNextCheckInProgressBar: boolean; 100 | hideHamburgerMenu: boolean; 101 | hideBottomMenu: boolean; 102 | automaticScroll: boolean; 103 | automaticScrollTimeMultiplier: number; 104 | automaticScrollWaitSeconds: number; 105 | showMiniMap: boolean; 106 | miniMapWidth: number; 107 | } 108 | 109 | export interface VersionCheck { 110 | version: number; 111 | version_string: string; 112 | } -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "types": ["vitest/globals", "@testing-library/jest-dom", "vite/client", "vite-plugin-svgr/client"], 6 | "allowJs": true, 7 | "skipLibCheck": false, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "module": "ESNext", 14 | "moduleResolution": "node", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "noEmit": true, 18 | "jsx": "react-jsx", 19 | "baseUrl": ".", 20 | "paths": { 21 | "atoms/*": ["src/atoms/*"], 22 | "components/*": ["src/components/*"], 23 | "helpers/*": ["src/helpers/*"], 24 | "types/*": ["src/types/*"], 25 | "widgets/*": ["src/widgets/*"], 26 | "utils/*": ["src/utils/*"] 27 | } 28 | }, 29 | "include": ["src"] 30 | } -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig, transformWithEsbuild } from 'vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import viteTsconfigPaths from 'vite-tsconfig-paths'; 5 | import svgr from 'vite-plugin-svgr'; 6 | import { VitePWA } from 'vite-plugin-pwa'; 7 | import path from "path"; 8 | 9 | // https://vitejs.dev/config/ 10 | export default defineConfig({ 11 | base: './', // needed to work in subdirectories 12 | build: { 13 | outDir: 'build', 14 | }, 15 | optimizeDeps: { 16 | force: true, 17 | esbuildOptions: { 18 | loader: { 19 | '.js': 'jsx', 20 | }, 21 | }, 22 | }, 23 | plugins: [ 24 | react({ 25 | // Enable JSX in .js files 26 | include: /\.(jsx|js|tsx|ts)$/, 27 | }), 28 | viteTsconfigPaths(), 29 | svgr({ 30 | include: '**/*.svg?react', 31 | }), 32 | VitePWA({ 33 | injectRegister: 'auto' 34 | }), 35 | ], 36 | resolve: { 37 | // alias. These also need to be set up in tsconfig.json 38 | alias: { 39 | '~': path.resolve(__dirname, './src'), 40 | 'atoms': path.resolve(__dirname, 'src/atoms'), 41 | 'components': path.resolve(__dirname, 'src/components'), 42 | 'helpers': path.resolve(__dirname, 'src/helpers'), 43 | 'types': path.resolve(__dirname, 'src/types'), 44 | 'widgets': path.resolve(__dirname, 'src/widgets'), 45 | }, 46 | }, 47 | server: { 48 | open: false, // BROWSER=false 49 | port: 3015, 50 | }, 51 | test: { 52 | globals: true, 53 | environment: 'jsdom', 54 | setupFiles: ['./src/setupTests.ts'], 55 | coverage: { 56 | reporter: ['text', 'html'], 57 | exclude: [ 58 | 'node_modules/', 59 | 'src/setupTests.ts', 60 | ], 61 | }, 62 | }, 63 | }); --------------------------------------------------------------------------------