Pushover and API Change truenas
This commit is contained in:
@@ -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
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user