Read-only monitoring/alerting for boilers, water softener, calorifiers and pumps via a generic MQTT-topic-prefix asset model, so new equipment can be onboarded without new ingestion code. Threshold and stale-data alert rules with email + in-app notification. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
73 lines
2.7 KiB
TypeScript
73 lines
2.7 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { NavLink, useLocation } from 'react-router-dom'
|
|
import { Gauge, LayoutGrid, Wrench, Bell, Settings, Menu, LogOut } from 'lucide-react'
|
|
import { useAuth } from './AuthGate'
|
|
import { can } from '../types'
|
|
|
|
const ICON_PROPS = { size: 16, strokeWidth: 1.75 }
|
|
|
|
const NAV = [
|
|
{ to: '/dashboard', label: 'Dashboard', icon: LayoutGrid, cap: 'view' },
|
|
{ to: '/assets', label: 'Assets', icon: Wrench, cap: 'manage_assets' },
|
|
{ to: '/alerts', label: 'Alerts', icon: Bell, cap: 'view' },
|
|
{ to: '/settings', label: 'Settings', icon: Settings, cap: 'settings' },
|
|
]
|
|
|
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
const { user } = useAuth()
|
|
const items = NAV.filter(n => can(user, n.cap))
|
|
const location = useLocation()
|
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
|
|
async function logout() {
|
|
await fetch('/plant/api/auth/logout', { method: 'POST', credentials: 'include' })
|
|
window.location.reload()
|
|
}
|
|
|
|
useEffect(() => { setMenuOpen(false) }, [location.pathname])
|
|
|
|
return (
|
|
<div className={`app-shell${menuOpen ? ' menu-open' : ''}`}>
|
|
<aside className="sidebar">
|
|
<div className="sidebar-logo">
|
|
<Gauge size={18} strokeWidth={1.75} />
|
|
Plant Room
|
|
</div>
|
|
<nav className="sidebar-nav">
|
|
{items.map(({ to, label, icon: Icon }) => (
|
|
<NavLink key={to} to={to} className={({ isActive }) => isActive ? 'active' : ''}>
|
|
<Icon {...ICON_PROPS} />
|
|
{label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
<div className="sidebar-user" style={{ whiteSpace: 'normal' }}>
|
|
<div style={{ fontWeight: 600, color: 'var(--text)', fontSize: '12px', marginBottom: '2px' }}>{user.name}</div>
|
|
<div style={{ fontSize: '11px', marginBottom: '8px' }}>{user.email}</div>
|
|
<button onClick={logout} style={{
|
|
display: 'flex', alignItems: 'center', gap: '6px',
|
|
background: 'none', border: 'none', color: 'inherit',
|
|
fontSize: '12px', padding: 0, cursor: 'pointer',
|
|
}}>
|
|
<LogOut size={13} strokeWidth={1.75} />
|
|
Sign out
|
|
</button>
|
|
</div>
|
|
</aside>
|
|
|
|
{menuOpen && <div className="menu-backdrop" onClick={() => setMenuOpen(false)} />}
|
|
|
|
<header className="top-bar">
|
|
<button className="top-bar-burger" onClick={() => setMenuOpen(o => !o)}>
|
|
<Menu size={20} strokeWidth={1.75} />
|
|
</button>
|
|
<Gauge size={18} strokeWidth={1.75} color="var(--gold)" />
|
|
<span className="top-bar-title">Plant Room</span>
|
|
</header>
|
|
|
|
<main className="page-content">
|
|
{children}
|
|
</main>
|
|
</div>
|
|
)
|
|
}
|