feat: Add real-time notification center with WebSocket integration

- Add in-app notification system with database storage
- Implement bell icon component with unread count badge in dashboard header
- Add full Notifications page tab with pagination and filtering (All/Unread/Read)
- Implement real-time WebSocket push notifications to all connected clients
- Add notification API endpoints: GET, PUT /read, DELETE, POST /clear
- Fix critical bug in notificationService where 'status' variable was undefined
- Add Notifications route to App.js routing
- Enhanced UI with notification popup in header and dedicated full-page view
- Auto-reconnect WebSocket with 5s timeout on disconnect
- Support rich notification content with resource details and timestamps
This commit is contained in:
nmemmert committed 2026-01-14 14:51:29 -05:00
1 parent 7968c9231e
commit db85573986
37 files changed
+3521 -207

No files matched your search

+35 -2
View File
@@ -5,6 +5,7 @@ import './App.css';
function SLA() {
const [slaData, setSlaData] = useState([]);
const [days, setDays] = useState(30);
const [maxDays, setMaxDays] = useState(30);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [currentPage, setCurrentPage] = useState(1);
@@ -13,6 +14,25 @@ function SLA() {
const [effectiveDays, setEffectiveDays] = useState(30);
const [limitedByRetention, setLimitedByRetention] = useState(false);
useEffect(() => {
// Fetch retention setting from server
const fetchRetention = async () => {
try {
const response = await axios.get('/api/settings');
const retentionDays = response.data.retention_days || 30;
setMaxDays(parseInt(retentionDays));
// Adjust current days if it exceeds retention
if (days > retentionDays) {
setDays(parseInt(retentionDays));
}
} catch (error) {
// Failed to fetch retention setting, use default
}
};
fetchRetention();
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
const loadSLAData = React.useCallback(async () => {
setLoading(true);
setError(null);
@@ -25,7 +45,6 @@ function SLA() {
setLimitedByRetention(Boolean(response.data.limited_by_retention));
setLoading(false);
} catch (error) {
console.error('Error loading SLA data:', error);
setError(error.message);
setLoading(false);
}
@@ -43,11 +62,23 @@ function SLA() {
meetingTarget: slaData.filter(s => s.meets_target).length,
avgUptime: (slaData.reduce((sum, s) => sum + parseFloat(s.actual_uptime), 0) / slaData.length).toFixed(2),
totalIncidents: slaData.reduce((sum, s) => sum + s.incidents, 0),
totalDowntime: slaData.reduce((sum, s) => sum + s.downtime_minutes, 0),
totalDowntime: (slaData.reduce((sum, s) => sum + s.downtime_minutes, 0) / 60).toFixed(2), // Convert to hours
} : null;
return (
<div className="container">
{limitedByRetention && (
<div style={{
padding: '1rem',
marginBottom: '1rem',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
color: '#856404'
}}>
⚠️ SLA data limited to {effectiveDays} days (server retention setting). Requested {days} days.
</div>
)}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '2rem', flexWrap: 'wrap', gap: '1rem' }}>
<h2>SLA Dashboard</h2>
<div style={{ display: 'flex', gap: '0.5rem' }}>
@@ -56,6 +87,8 @@ function SLA() {
key={d}
className={`btn ${days === d ? 'btn-primary' : 'btn-secondary'}`}
onClick={() => setDays(d)}
disabled={d > maxDays}
style={{ opacity: d > maxDays ? 0.5 : 1 }}
>
{d} Days
</button>