Skip to content

Commit 9dee090

Browse files
Merge pull request #119 from chriscareycode/2026-01-04
2026 01 04
2 parents 80c1f26 + 0d994e7 commit 9dee090

29 files changed

Lines changed: 1416 additions & 799 deletions

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Homepage at [NagiosTV.com](https://nagiostv.com)
44

5-
![Display](https://nagiostv.com/images/nagiostv-0.9.0-short.png)
5+
![Display](https://nagiostv.com/images/nagiostv-0.9.9-beta.png)
66

77
NagiosTV is a user interface add-on for the Nagios monitoring system https://www.nagios.org
88

package-lock.json

Lines changed: 557 additions & 696 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,46 +5,48 @@
55
"private": true,
66
"homepage": "./",
77
"devDependencies": {
8-
"@tailwindcss/vite": "^4.1.17",
9-
"@testing-library/jest-dom": "^6.6.3",
8+
"@tailwindcss/vite": "^4.1.18",
9+
"@testing-library/jest-dom": "^6.9.1",
1010
"@testing-library/react": "^15.0.7",
11-
"@types/jquery": "^3.5.32",
11+
"@types/jquery": "^3.5.33",
1212
"@types/js-cookie": "^3.0.6",
13-
"@types/lodash": "^4.17.18",
14-
"@types/luxon": "^3.6.2",
15-
"@types/node": "^18.19.112",
16-
"@types/react": "^18.3.23",
13+
"@types/lodash": "^4.17.21",
14+
"@types/luxon": "^3.7.1",
15+
"@types/node": "^18.19.130",
16+
"@types/react": "^18.3.27",
1717
"@types/react-dom": "^18.3.7",
18-
"@vitejs/plugin-react": "^4.5.2",
18+
"@vitejs/plugin-react": "^4.7.0",
1919
"c8": "^9.1.0",
2020
"jsdom": "^24.1.3",
21-
"tailwindcss": "^4.1.17",
22-
"typescript": "^5.8.3",
21+
"tailwindcss": "^4.1.18",
22+
"typescript": "^5.9.3",
2323
"vite": "^6.4.1",
2424
"vite-plugin-pwa": "^0.21.2",
25-
"vite-plugin-svgr": "^4.3.0",
25+
"vite-plugin-svgr": "^4.5.0",
2626
"vite-tsconfig-paths": "^5.1.4",
2727
"vitest": "^3.2.4"
2828
},
2929
"dependencies": {
3030
"@fortawesome/fontawesome-svg-core": "^6.7.2",
3131
"@fortawesome/free-solid-svg-icons": "^6.7.2",
32-
"@fortawesome/react-fontawesome": "^0.2.2",
33-
"allotment": "^1.20.4",
34-
"axios": "^1.12.0",
32+
"@fortawesome/react-fontawesome": "^0.2.6",
33+
"@types/qs": "^6.14.0",
34+
"allotment": "^1.20.5",
35+
"axios": "^1.13.2",
3536
"clipboard-polyfill": "^3.0.3",
36-
"highcharts": "^12.3.0",
37-
"highcharts-react-official": "^3.2.2",
38-
"html2canvas-pro": "^1.5.13",
39-
"jotai": "^2.12.5",
37+
"highcharts": "^12.4.0",
38+
"highcharts-react-official": "^3.2.3",
39+
"html2canvas-pro": "^1.6.4",
40+
"jotai": "^2.16.1",
4041
"js-cookie": "^3.0.5",
4142
"lodash": "^4.17.21",
42-
"luxon": "^3.6.1",
43+
"luxon": "^3.7.2",
4344
"motion": "^11.18.2",
45+
"qs": "^6.14.1",
4446
"react": "^18.3.1",
4547
"react-app-polyfill": "^3.0.0",
4648
"react-dom": "^18.3.1",
47-
"react-router-dom": "^6.30.1",
49+
"react-router-dom": "^6.30.3",
4850
"react-tooltip": "^4.5.1",
4951
"styled-components": "^5.3.11",
5052
"url-search-params-polyfill": "^8.2.5"

src/atoms/llmAtom.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface LLMHistoryItem {
3535
emoji: string;
3636
model: string; // The LLM model used for this response
3737
color: LLMHistoryColor;
38+
shortResponse: string; // Short response for Doomguy speech balloon
3839
}
3940

4041
// History state

src/atoms/settingsState.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,25 @@ Alerts are always in the past, and I like to measure how long since the last ale
169169
- If we are not acknowledged, not scheduled downtime, or not flapping, do not mention these states in the response.
170170
- If we have flapping, put a emoji related to flapping, next to the where you call it out.
171171
172-
Do not provide recommendations unless they are explicitly called out below:
173-
174-
======================================
175-
RECOMMENDATIONS if the service is not in OK state:
176-
177-
- Check APT: Update the packages at your earliest convenience.
178-
======================================
179-
180172
`,
181173

182174
// Server settings
183175
serverSettingsTakePrecedence: false,
184176
};
185177

186-
export const bigStateAtom = atom(bigStateInitial);
178+
// Store atoms in a global cache to preserve them across HMR
179+
// This prevents the "Settings are not loaded yet" issue during hot-reload
180+
// interface AtomCache {
181+
// bigStateAtom?: ReturnType<typeof atom<BigState>>;
182+
// clientSettingsAtom?: ReturnType<typeof atom<ClientSettings>>;
183+
// }
187184

185+
// const globalWithCache = globalThis as typeof globalThis & { __NAGIOSTV_ATOM_CACHE__?: AtomCache };
186+
// const atomCache: AtomCache = globalWithCache.__NAGIOSTV_ATOM_CACHE__ ??= {};
187+
188+
// export const bigStateAtom = atomCache.bigStateAtom ??= atom(bigStateInitial);
189+
// export const clientSettingsAtom = atomCache.clientSettingsAtom ??= atom(clientSettingsInitial);
190+
191+
export const bigStateAtom = atom(bigStateInitial);
188192
export const clientSettingsAtom = atom(clientSettingsInitial);
193+

src/components/Doomguy/Doomguy.css

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,41 @@
141141
color: #888;
142142
opacity: 0.7;
143143
white-space: nowrap;
144+
}
145+
146+
.doomguy-speech-balloon {
147+
position: absolute;
148+
bottom: 100%;
149+
right: 0;
150+
margin-bottom: 8px;
151+
padding: 6px 10px;
152+
background-color: #2a2a2a;
153+
border: 1px solid #555;
154+
border-radius: 12px;
155+
font-size: 12px;
156+
color: #eee;
157+
white-space: nowrap;
158+
box-shadow: 2px 2px 4px rgba(0, 0, 0, 0.4);
159+
z-index: 10;
160+
}
161+
162+
/* Speech balloon tail/pointer */
163+
.doomguy-speech-balloon::after {
164+
content: '';
165+
position: absolute;
166+
bottom: -8px;
167+
right: 15px;
168+
border-width: 8px 6px 0 6px;
169+
border-style: solid;
170+
border-color: #2a2a2a transparent transparent transparent;
171+
}
172+
173+
.doomguy-speech-balloon::before {
174+
content: '';
175+
position: absolute;
176+
bottom: -10px;
177+
right: 14px;
178+
border-width: 9px 7px 0 7px;
179+
border-style: solid;
180+
border-color: #555 transparent transparent transparent;
144181
}

src/components/Doomguy/Doomguy.tsx

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import { useAtomValue } from 'jotai';
2323
import { clientSettingsAtom } from '../../atoms/settingsState';
2424
import { hostAtom } from '../../atoms/hostAtom';
2525
import { serviceAtom } from '../../atoms/serviceAtom';
26-
import { llmIsLoadingAtom } from '../../atoms/llmAtom';
26+
import { llmIsLoadingAtom, llmHistoryAtom, llmCurrentHistoryIndexAtom } from '../../atoms/llmAtom';
2727

2828
// Helpers
2929
import {
@@ -42,9 +42,10 @@ import doomguyImage from './Doomguy.png';
4242
Doomguy will be bloody at >= 4 services down
4343
*/
4444

45-
const Doomguy = ({ scaleCss, style }: {
45+
const Doomguy = ({ scaleCss, style, showBalloon = true }: {
4646
scaleCss?: string,
47-
style?: React.CSSProperties
47+
style?: React.CSSProperties,
48+
showBalloon?: boolean
4849
}) => {
4950

5051
const smileClasses = ['doomguy20', 'doomguy21', 'doomguy22', 'doomguy23'];
@@ -58,21 +59,50 @@ const Doomguy = ({ scaleCss, style }: {
5859
const hostState = useAtomValue(hostAtom);
5960
const serviceState = useAtomValue(serviceAtom);
6061
const llmIsLoading = useAtomValue(llmIsLoadingAtom);
62+
const llmHistory = useAtomValue(llmHistoryAtom);
63+
const llmCurrentHistoryIndex = useAtomValue(llmCurrentHistoryIndexAtom);
64+
65+
// Get the current history item for shortResponse and color
66+
const currentHistoryItem = llmHistory[llmCurrentHistoryIndex];
67+
const llmShortResponse = currentHistoryItem?.shortResponse || '';
68+
const llmHistoryColor = currentHistoryItem?.color || 'green';
6169

6270
const [clicked, setClicked] = useState(false); // Clicking his face will temporarily make him angry
6371
const [thinkingFrame, setThinkingFrame] = useState(thinkingAnimation[0]);
6472

6573
// Calculate filtered counts using useMemo for performance
66-
const howManyDown = useMemo(() => {
74+
// howManyDown includes hosts down + service warnings + service criticals (used for concerned/angry)
75+
// howManyDownBloody excludes service warnings (used for bloody mode threshold)
76+
const { howManyDown, howManyDownBloody } = useMemo(() => {
6777
const filteredHosts = filterHostStateArray(hostState.stateArray, clientSettings);
6878
const filteredServices = filterServiceStateArray(serviceState.stateArray, clientSettings);
6979

7080
const hostDownCount = countFilteredHostStates(filteredHosts);
7181
const serviceDownCount = countFilteredServiceStates(filteredServices);
7282

73-
return hostDownCount + serviceDownCount.warning + serviceDownCount.critical;
83+
return {
84+
howManyDown: hostDownCount + serviceDownCount.warning + serviceDownCount.critical,
85+
howManyDownBloody: hostDownCount + serviceDownCount.critical, // excludes warnings for bloody mode
86+
};
7487
}, [hostState.stateArray, serviceState.stateArray, clientSettings]);
7588

89+
// Map the LLM history color to CSS color
90+
const speechBalloonColor = useMemo(() => {
91+
switch (llmHistoryColor) {
92+
case 'red':
93+
return '#FD7272';
94+
case 'orange':
95+
return 'orange';
96+
case 'yellow':
97+
return 'yellow';
98+
case 'gray':
99+
return 'gray';
100+
case 'green':
101+
default:
102+
return 'lime';
103+
}
104+
}, [llmHistoryColor]);
105+
76106
// Animate Doomguy while thinking
77107
useEffect(() => {
78108
if (!llmIsLoading) {
@@ -92,10 +122,11 @@ const Doomguy = ({ scaleCss, style }: {
92122
classes = smileClasses;
93123
} else if (howManyDown === 0) {
94124
classes = happyClasses;
95-
} else if (howManyDown >= clientSettings.doomguyAngryAt && howManyDown < clientSettings.doomguyBloodyAt) {
96-
classes = angryClasses;
97-
} else if (howManyDown >= clientSettings.doomguyBloodyAt) {
125+
} else if (howManyDownBloody >= clientSettings.doomguyBloodyAt) {
126+
// Check bloody first (uses count excluding warnings)
98127
classes = bloodyClasses;
128+
} else if (howManyDown >= clientSettings.doomguyAngryAt) {
129+
classes = angryClasses;
99130
} else {
100131
classes = happyClasses;
101132
}
@@ -134,6 +165,11 @@ const Doomguy = ({ scaleCss, style }: {
134165

135166
return (
136167
<div className="doomguy-wrap" style={style}>
168+
{showBalloon && llmShortResponse && !llmIsLoading && (
169+
<div className="doomguy-speech-balloon" style={{ color: speechBalloonColor }}>
170+
{llmShortResponse}
171+
</div>
172+
)}
137173
<div style={transformCss} className={doomguyClass} onClick={clickedDoomguy}>
138174
</div>
139175
{llmIsLoading && <div className="doomguy-thinking">thinking</div>}

src/components/Settings.tsx

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
3636
import { faExclamationTriangle, faTools } from '@fortawesome/free-solid-svg-icons';
3737
import { ClientSettings } from 'types/settings';
3838
import Doomguy from './Doomguy/Doomguy';
39+
import LlmModelSelector from './settings/LlmModelSelector';
3940

4041
const Settings = () => {
4142

@@ -863,7 +864,7 @@ const Settings = () => {
863864
<span style={{ position: 'relative' }}> &nbsp; The character from the 1993 video game Doom
864865

865866
<span style={{ position: 'absolute', top: 0, right: -56, height: 32, width: 24 }}>
866-
<Doomguy scaleCss='0.5' style={{ position: 'absolute', top: -13 }} />
867+
<Doomguy scaleCss='0.5' style={{ position: 'absolute', top: -13 }} showBalloon={false} />
867868
</span>
868869
</span>
869870
</td>
@@ -885,7 +886,7 @@ const Settings = () => {
885886
</tr>}
886887
{clientSettingsTemp.doomguyEnabled && <tr>
887888
<th>Doomguy bloody at</th>
888-
<td><input type="number" min="0" max="100" value={clientSettingsTemp.doomguyBloodyAt} onChange={handleChange('doomguyBloodyAt', 'number')} /> hosts DOWN, services WARNING or CRITICAL</td>
889+
<td><input type="number" min="0" max="100" value={clientSettingsTemp.doomguyBloodyAt} onChange={handleChange('doomguyBloodyAt', 'number')} /> hosts DOWN, services CRITICAL</td>
889890
</tr>}
890891
</tbody>
891892
</table>
@@ -960,16 +961,12 @@ const Settings = () => {
960961
<tr>
961962
<th>LLM Model:</th>
962963
<td>
963-
<input
964-
type="text"
965-
value={clientSettingsTemp.llmModel}
964+
<LlmModelSelector
965+
llmModel={clientSettingsTemp.llmModel}
966+
llmServerBaseUrl={clientSettingsTemp.llmServerBaseUrl}
967+
llmApiKey={clientSettingsTemp.llmApiKey}
966968
onChange={handleChange('llmModel', 'string')}
967-
placeholder="llama2 or gpt-3.5-turbo"
968969
/>
969-
<br />
970-
<span style={{ fontSize: '0.9em', color: '#888' }}>
971-
Model name to use (e.g., llama2, mistral, gpt-3.5-turbo)
972-
</span>
973970
</td>
974971
</tr>
975972
<tr>

src/components/alerts/AlertFilters.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { bigStateAtom, clientSettingsAtom } from '../../atoms/settingsState';
2323
import { translate } from '../../helpers/language';
2424
import Checkbox from '../widgets/FilterCheckbox';
2525
import { saveLocalStorage } from 'helpers/nagiostv';
26+
import { useQueryParams } from '../../hooks/useQueryParams';
2627
// CSS
2728
import './AlertFilters.css';
2829

@@ -36,6 +37,7 @@ const AlertFilters = ({
3637

3738
const bigState = useAtomValue(bigStateAtom);
3839
const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom);
40+
const queryParams = useQueryParams();
3941

4042
// Chop the bigState into vars
4143
const {
@@ -76,6 +78,9 @@ const AlertFilters = ({
7678
[propName]: val,
7779
});
7880
});
81+
82+
// Sync to URL query params
83+
queryParams.set({ [propName]: val });
7984
};
8085

8186
return (

src/components/hosts/HostFilters.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,18 @@ import { hostHowManyAtom } from '../../atoms/hostAtom';
2424
import './HostFilters.css';
2525
import { translate } from '../../helpers/language';
2626
import FilterCheckbox from '../widgets/FilterCheckbox';
27+
import SortOrderSelect from '../widgets/SortOrderSelect';
2728
import { saveLocalStorage } from 'helpers/nagiostv';
2829
import { ChangeEvent } from 'react';
30+
import { useQueryParams } from '../../hooks/useQueryParams';
2931

3032
const HostFilters = () => {
3133

3234
const hostHowManyState = useAtomValue(hostHowManyAtom);
3335

3436
const bigState = useAtomValue(bigStateAtom);
3537
const [clientSettings, setClientSettings] = useAtom(clientSettingsAtom);
38+
const queryParams = useQueryParams();
3639

3740
// Chop the bigState into vars
3841
const {
@@ -92,6 +95,8 @@ const HostFilters = () => {
9295
});
9396
});
9497

98+
// Sync to URL query params
99+
queryParams.set({ [propName]: val });
95100
};
96101

97102
const {
@@ -110,10 +115,13 @@ const HostFilters = () => {
110115
return (
111116
<>
112117

113-
{!hideFilters && <select value={hostSortOrder} data-varname={'hostSortOrder'} onChange={handleSelectChange}>
114-
<option value="newest">{translate('newest first', language)}</option>
115-
<option value="oldest">{translate('oldest first', language)}</option>
116-
</select>}
118+
{!hideFilters && <SortOrderSelect
119+
value={hostSortOrder}
120+
varName="hostSortOrder"
121+
language={language}
122+
onChange={handleSelectChange}
123+
syncToUrl={true}
124+
/>}
117125

118126
<span>
119127
{' '}

0 commit comments

Comments
 (0)