Pushover and API Change truenas

This commit is contained in:
2026-01-27 09:59:32 +00:00
parent 945125b069
commit 3e6132c1bb
29 changed files with 6566 additions and 66 deletions
+48
View File
@@ -194,6 +194,54 @@ body {
gap: 12px;
}
.drawer-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.drawer-section-title {
font-weight: 700;
color: var(--ink);
}
.drawer-form {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
}
.drawer-actions {
justify-content: flex-start;
gap: 0.75rem;
}
.drawer-hint {
color: var(--ink-soft);
font-size: 0.9rem;
}
.drawer-list {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.drawer-list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.35rem 0;
border-bottom: 1px dashed rgba(148, 163, 184, 0.35);
}
.drawer-list-item:last-child {
border-bottom: none;
}
.drawer-list-label {
font-weight: 600;
color: var(--ink);
}
.app-content {
margin-top: 2rem;
display: flex;
+359 -2
View File
@@ -74,8 +74,8 @@ const formatCapacity = (bytes, base, fixedDecimals = false) => {
const useGb = Math.abs(safeBytes) < tbDivisor;
const value = safeBytes / (useGb ? gbDivisor : tbDivisor);
const unit = useGb
? (base === 1024 ? 'GiB' : 'GB')
: (base === 1024 ? 'TiB' : 'TB');
? (base === 1024 ? 'GB' : 'GB')
: (base === 1024 ? 'TB' : 'TB');
return `${new Intl.NumberFormat('en-US', formatOptions).format(value)} ${unit}`;
};
@@ -116,6 +116,13 @@ export default function App() {
const [maxColumns, setMaxColumns] = useState(4);
const gridRef = useRef(null);
const [showSettingsSheet, setShowSettingsSheet] = useState(false);
const [alertsConfig, setAlertsConfig] = useState(null);
const [alertsSaving, setAlertsSaving] = useState(false);
const [alertsError, setAlertsError] = useState('');
const [alertsTestState, setAlertsTestState] = useState('idle');
const [serverAlertsOpen, setServerAlertsOpen] = useState(false);
const [serverAlertsServer, setServerAlertsServer] = useState(null);
const [serverAlertsDraft, setServerAlertsDraft] = useState(null);
useEffect(() => {
const resolveColumns = () => {
@@ -168,8 +175,23 @@ export default function App() {
}
};
const loadAlerts = async () => {
try {
const response = await fetch('/api/alerts');
if (!response.ok) {
throw new Error('Alerts config could not be loaded.');
}
const data = await response.json();
setAlertsConfig(data?.alerts || null);
setAlertsError('');
} catch (error) {
setAlertsError(error.message || 'Alerts config could not be loaded.');
}
};
useEffect(() => {
loadStorage();
loadAlerts();
}, []);
const totals = useMemo(() => {
@@ -682,6 +704,101 @@ export default function App() {
}
};
const mergeAlerts = (base, override) => ({
enabled: override?.enabled ?? base?.enabled ?? true,
warnPercent: override?.warnPercent ?? base?.warnPercent ?? 80,
critPercent: override?.critPercent ?? base?.critPercent ?? 90,
cooldownMinutes: override?.cooldownMinutes ?? base?.cooldownMinutes ?? 60,
once: override?.once ?? base?.once ?? false,
includePools: override?.includePools ?? base?.includePools ?? true
});
const openServerAlerts = (server) => {
const base = alertsConfig?.defaults || null;
setServerAlertsServer(server);
setServerAlertsDraft(mergeAlerts(base, server.alerts));
setServerAlertsOpen(true);
};
const handleSaveAlerts = async () => {
if (!alertsConfig) {
return;
}
setAlertsSaving(true);
setAlertsError('');
try {
const payload = {
enabled: alertsConfig.enabled,
pushover: {
userKey: alertsConfig?.pushover?.userKey || '',
apiToken: alertsConfig?.pushover?.apiToken || ''
},
defaults: {
enabled: alertsConfig?.defaults?.enabled ?? true,
warnPercent: alertsConfig?.defaults?.warnPercent ?? 80,
critPercent: alertsConfig?.defaults?.critPercent ?? 90,
cooldownMinutes: alertsConfig?.defaults?.cooldownMinutes ?? 60,
once: alertsConfig?.defaults?.once ?? false,
includePools: alertsConfig?.defaults?.includePools ?? true
}
};
const response = await fetch('/api/alerts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Alerts konnten nicht gespeichert werden.');
}
await loadAlerts();
} catch (error) {
setAlertsError(error.message || 'Alerts konnten nicht gespeichert werden.');
} finally {
setAlertsSaving(false);
}
};
const handleTestAlert = async () => {
setAlertsTestState('loading');
setAlertsError('');
try {
const response = await fetch('/api/alerts/test', { method: 'POST' });
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Test-Alert fehlgeschlagen.');
}
setAlertsTestState('success');
window.setTimeout(() => setAlertsTestState('idle'), 2000);
} catch (error) {
setAlertsTestState('error');
setAlertsError(error.message || 'Test-Alert fehlgeschlagen.');
}
};
const handleSaveServerAlerts = async () => {
if (!serverAlertsServer || !serverAlertsDraft) {
return;
}
try {
const response = await fetch(`/api/servers/${serverAlertsServer.id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ alerts: serverAlertsDraft })
});
if (!response.ok) {
const data = await response.json().catch(() => ({}));
throw new Error(data.error || 'Server-Alerts konnten nicht gespeichert werden.');
}
setServerAlertsOpen(false);
setServerAlertsServer(null);
setServerAlertsDraft(null);
await loadStorage();
} catch (error) {
setErrors((prev) => [...prev, { message: error.message || 'Server-Alerts konnten nicht gespeichert werden.' }]);
}
};
const sourceMeta = {
live: { label: 'Live', severity: 'success' },
empty: { label: 'Leer', severity: 'warning' },
@@ -711,6 +828,7 @@ export default function App() {
<InputSwitch checked={fixedDecimals} onChange={(event) => setFixedDecimals(Boolean(event.value))} />
</div>
<Button label="Refresh" icon="pi pi-sync" className="p-button-text" onClick={loadStorage} />
<Button label="Settings" icon="pi pi-cog" className="p-button-text" onClick={() => setShowSettingsSheet(true)} />
<Button label="Server anlegen" icon="pi pi-plus" severity="secondary" onClick={handleOpenWizard} />
</div>
);
@@ -807,11 +925,250 @@ export default function App() {
<span>2 Dezimalstellen</span>
<InputSwitch checked={fixedDecimals} onChange={(event) => setFixedDecimals(Boolean(event.value))} />
</div>
<Divider />
<div className="drawer-section">
<div className="drawer-section-title">Alerts (Pushover)</div>
{alertsError ? <Message severity="error" text={alertsError} /> : null}
{alertsConfig ? (
<div className="form-grid drawer-form">
<div className="form-field">
<label htmlFor="alerts-enabled">Alerts global aktiv</label>
<InputSwitch
id="alerts-enabled"
checked={Boolean(alertsConfig.enabled)}
onChange={(event) => setAlertsConfig((prev) => ({
...prev,
enabled: Boolean(event.value)
}))}
/>
</div>
<div className="form-field">
<label htmlFor="alerts-user-key">Pushover User Key</label>
<InputText
id="alerts-user-key"
value={alertsConfig?.pushover?.userKey || ''}
onChange={(event) => setAlertsConfig((prev) => ({
...prev,
pushover: { ...(prev?.pushover || {}), userKey: event.target.value }
}))}
placeholder="uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
/>
</div>
<div className="form-field">
<label htmlFor="alerts-api-token">Pushover API Token</label>
<InputText
id="alerts-api-token"
type="password"
value={alertsConfig?.pushover?.apiToken || ''}
onChange={(event) => setAlertsConfig((prev) => ({
...prev,
pushover: { ...(prev?.pushover || {}), apiToken: event.target.value }
}))}
placeholder="a1b2c3d4e5f6g7h8i9"
/>
</div>
<div className="form-field">
<label htmlFor="alerts-warn">Warnung ab %</label>
<InputNumber
id="alerts-warn"
value={alertsConfig?.defaults?.warnPercent ?? 80}
onValueChange={(event) => setAlertsConfig((prev) => ({
...prev,
defaults: { ...(prev?.defaults || {}), warnPercent: event.value ?? 0 }
}))}
min={1}
max={99}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="alerts-crit">Kritisch ab %</label>
<InputNumber
id="alerts-crit"
value={alertsConfig?.defaults?.critPercent ?? 90}
onValueChange={(event) => setAlertsConfig((prev) => ({
...prev,
defaults: { ...(prev?.defaults || {}), critPercent: event.value ?? 0 }
}))}
min={2}
max={100}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="alerts-cooldown">Cooldown (Minuten)</label>
<InputNumber
id="alerts-cooldown"
value={alertsConfig?.defaults?.cooldownMinutes ?? 60}
onValueChange={(event) => setAlertsConfig((prev) => ({
...prev,
defaults: { ...(prev?.defaults || {}), cooldownMinutes: event.value ?? 0 }
}))}
min={0}
max={525600}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="alerts-once">Nur einmal (bis Entwarnung)</label>
<InputSwitch
id="alerts-once"
checked={Boolean(alertsConfig?.defaults?.once)}
onChange={(event) => setAlertsConfig((prev) => ({
...prev,
defaults: { ...(prev?.defaults || {}), once: Boolean(event.value) }
}))}
/>
</div>
<div className="form-field">
<label htmlFor="alerts-pools">Pools mit pruefen</label>
<InputSwitch
id="alerts-pools"
checked={alertsConfig?.defaults?.includePools !== false}
onChange={(event) => setAlertsConfig((prev) => ({
...prev,
defaults: { ...(prev?.defaults || {}), includePools: Boolean(event.value) }
}))}
/>
</div>
<div className="form-actions drawer-actions">
<Button
label={alertsSaving ? 'Speichern...' : 'Speichern'}
icon="pi pi-save"
onClick={handleSaveAlerts}
disabled={alertsSaving}
/>
<Button
label={alertsTestState === 'loading' ? 'Teste...' : alertsTestState === 'success' ? 'Gesendet' : 'Test-Alert'}
icon="pi pi-send"
className="p-button-text"
onClick={handleTestAlert}
disabled={alertsSaving || alertsTestState === 'loading'}
/>
</div>
</div>
) : (
<div className="drawer-hint">Alerts werden geladen...</div>
)}
</div>
<Divider />
<div className="drawer-section">
<div className="drawer-section-title">Server Overrides</div>
<div className="drawer-list">
{servers.map((server) => (
<div key={server.id} className="drawer-list-item">
<div className="drawer-list-label">{server.name || server.host}</div>
<Button
label="Alerts"
icon="pi pi-bell"
className="p-button-text"
onClick={() => openServerAlerts(server)}
/>
</div>
))}
</div>
</div>
</div>
</div>
</div>
) : null}
<Dialog
header={`Alerts: ${serverAlertsServer?.name || serverAlertsServer?.host || 'Server'}`}
visible={serverAlertsOpen}
onHide={() => setServerAlertsOpen(false)}
className="wizard-dialog"
draggable={false}
>
{serverAlertsDraft ? (
<>
<div className="form-grid">
<div className="form-field">
<label htmlFor="server-alerts-enabled">Alerts fuer diesen Server</label>
<InputSwitch
id="server-alerts-enabled"
checked={Boolean(serverAlertsDraft.enabled)}
onChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
enabled: Boolean(event.value)
}))}
/>
</div>
<div className="form-field">
<label htmlFor="server-alerts-warn">Warnung ab %</label>
<InputNumber
id="server-alerts-warn"
value={serverAlertsDraft.warnPercent}
onValueChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
warnPercent: event.value ?? prev.warnPercent
}))}
min={1}
max={99}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="server-alerts-crit">Kritisch ab %</label>
<InputNumber
id="server-alerts-crit"
value={serverAlertsDraft.critPercent}
onValueChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
critPercent: event.value ?? prev.critPercent
}))}
min={2}
max={100}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="server-alerts-cooldown">Cooldown (Minuten)</label>
<InputNumber
id="server-alerts-cooldown"
value={serverAlertsDraft.cooldownMinutes}
onValueChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
cooldownMinutes: event.value ?? prev.cooldownMinutes
}))}
min={0}
max={525600}
useGrouping={false}
/>
</div>
<div className="form-field">
<label htmlFor="server-alerts-once">Nur einmal</label>
<InputSwitch
id="server-alerts-once"
checked={Boolean(serverAlertsDraft.once)}
onChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
once: Boolean(event.value)
}))}
/>
</div>
<div className="form-field">
<label htmlFor="server-alerts-pools">Pools mit pruefen</label>
<InputSwitch
id="server-alerts-pools"
checked={serverAlertsDraft.includePools !== false}
onChange={(event) => setServerAlertsDraft((prev) => ({
...prev,
includePools: Boolean(event.value)
}))}
/>
</div>
</div>
<div className="form-actions">
<Button label="Abbrechen" className="p-button-text" onClick={() => setServerAlertsOpen(false)} />
<Button label="Speichern" icon="pi pi-check" onClick={handleSaveServerAlerts} />
</div>
</>
) : (
<div className="drawer-hint">Alerts werden geladen...</div>
)}
</Dialog>
<Dialog
header="Server anlegen"
visible={wizardOpen}