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

+38 -2
View File
@@ -22,6 +22,7 @@ const asNumberOrZero = (value) => {
function History() {
const [historyData, setHistoryData] = useState([]);
const [days, setDays] = useState(7);
const [maxDays, setMaxDays] = useState(30);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [showAveraged, setShowAveraged] = useState(true);
@@ -29,7 +30,28 @@ function History() {
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [totalItems, setTotalItems] = useState(0);
const [effectiveDays, setEffectiveDays] = useState(7);
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
useEffect(() => {
const loadHistory = async () => {
setLoading(true);
@@ -47,6 +69,8 @@ function History() {
if (response.data.resources && Array.isArray(response.data.resources)) {
data = response.data.resources;
total = response.data.total || data.length;
setEffectiveDays(response.data.effective_days || days);
setLimitedByRetention(Boolean(response.data.limited_by_retention));
} else if (Array.isArray(response.data)) {
data = response.data;
total = data.length;
@@ -61,7 +85,6 @@ function History() {
});
return { ...resource, trendsData: trendsResponse.data };
} catch (err) {
console.error(`Error loading trends for resource ${resource.id}:`, err);
return resource;
}
});
@@ -72,7 +95,6 @@ function History() {
setTotalItems(total);
setLoading(false);
} catch (error) {
console.error('Error loading history:', error);
setError(error.message);
setLoading(false);
}
@@ -86,6 +108,18 @@ function History() {
return (
<div className="container">
{limitedByRetention && (
<div style={{
padding: '1rem',
marginBottom: '1rem',
backgroundColor: '#fff3cd',
border: '1px solid #ffc107',
borderRadius: '4px',
color: '#856404'
}}>
⚠️ 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>Monitoring History</h2>
<div style={{ display: 'flex', gap: '1rem', alignItems: 'center', flexWrap: 'wrap' }}>
@@ -95,6 +129,8 @@ function History() {
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>