mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-17 19:36:28 +02:00
feat(notifications): per-event ntfy priority headers (#990)
ntfy supports a Priority header (1=min, 2=low, 3=default, 4=high, 5=urgent) that controls escalation on the receiving device, but every event was being sent at the server default — so a "50% complete" ping looked identical to "print failed" or "printer offline". Add a per-event priority dropdown section in the Add/Edit Notification modal (visible only for ntfy, listing only enabled events); the backend reads config.event_priorities and emits the matching Priority header on POST and PUT (image-attachment) paths. Unmapped events fall through to the ntfy server default. Out-of-range and non-numeric values are dropped, not clamped, so a misconfigured value never silently sends at the wrong urgency. Test sends omit the header by design so the test path can't accidentally page someone at urgent priority. Backward compatible: existing providers without event_priorities behave exactly as before. NtfyConfig.event_priorities is optional; the route stores config as a JSON blob so no migration is needed. i18n: full translations across all 8 locales (en/de/fr/it/ja/pt-BR/zh-CN/ zh-TW). README, CHANGELOG, and the wiki notifications page updated. Tests: 6 backend (Priority set on mapped, omitted on unmapped/missing/ no-priorities, ignored for bad values, propagated through attachment path), 6 frontend (section visible only for ntfy, lists only enabled events, save round-trip, edit pre-fill, toggle drops row, non-ntfy never writes the key).
This commit is contained in:
@@ -43,11 +43,32 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
|
||||
const [onBedCooled, setOnBedCooled] = useState(provider?.on_bed_cooled ?? false);
|
||||
const [onFirstLayerComplete, setOnFirstLayerComplete] = useState(provider?.on_first_layer_complete ?? false);
|
||||
|
||||
// Provider-specific config
|
||||
// Provider-specific config (scalar fields only — event_priorities is split out
|
||||
// into its own state because it's an object, not a string).
|
||||
const [config, setConfig] = useState<Record<string, string>>(
|
||||
provider?.config ? Object.fromEntries(Object.entries(provider.config).map(([k, v]) => [k, String(v)])) : {}
|
||||
provider?.config
|
||||
? Object.fromEntries(
|
||||
Object.entries(provider.config)
|
||||
.filter(([k]) => k !== 'event_priorities')
|
||||
.map(([k, v]) => [k, String(v)]),
|
||||
)
|
||||
: {},
|
||||
);
|
||||
|
||||
// Per-event ntfy priority (#990). Map of event key → 1-5. Persisted into
|
||||
// config.event_priorities on save; only sent when the provider is ntfy.
|
||||
const initialEventPriorities = (() => {
|
||||
const raw = provider?.config?.event_priorities;
|
||||
if (!raw || typeof raw !== 'object') return {} as Record<string, number>;
|
||||
const out: Record<string, number> = {};
|
||||
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
||||
const n = Number(v);
|
||||
if (Number.isInteger(n) && n >= 1 && n <= 5) out[k] = n;
|
||||
}
|
||||
return out;
|
||||
})();
|
||||
const [eventPriorities, setEventPriorities] = useState<Record<string, number>>(initialEventPriorities);
|
||||
|
||||
const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@@ -120,10 +141,15 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
|
||||
}
|
||||
}
|
||||
|
||||
const finalConfig: Record<string, unknown> =
|
||||
providerType === 'ntfy' && Object.keys(eventPriorities).length > 0
|
||||
? { ...config, event_priorities: eventPriorities }
|
||||
: config;
|
||||
|
||||
const data = {
|
||||
name: name.trim(),
|
||||
provider_type: providerType,
|
||||
config,
|
||||
config: finalConfig,
|
||||
printer_id: printerId,
|
||||
quiet_hours_enabled: quietHoursEnabled,
|
||||
quiet_hours_start: quietHoursEnabled ? quietHoursStart : null,
|
||||
@@ -527,6 +553,54 @@ export function AddNotificationModal({ provider, onClose }: AddNotificationModal
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Per-event ntfy priority (#990) */}
|
||||
{providerType === 'ntfy' && (() => {
|
||||
const enabledEvents: Array<{ key: string; label: string }> = [];
|
||||
if (onPrintStart) enabledEvents.push({ key: 'on_print_start', label: t('notifications.start') });
|
||||
if (onPrintComplete) enabledEvents.push({ key: 'on_print_complete', label: t('notifications.complete') });
|
||||
if (onPrintFailed) enabledEvents.push({ key: 'on_print_failed', label: t('notifications.failed') });
|
||||
if (onPrintStopped) enabledEvents.push({ key: 'on_print_stopped', label: t('notifications.stopped') });
|
||||
if (onPrintProgress) enabledEvents.push({ key: 'on_print_progress', label: t('notifications.progress') });
|
||||
if (onBedCooled) enabledEvents.push({ key: 'on_bed_cooled', label: t('notifications.bedCooled') });
|
||||
if (onFirstLayerComplete) enabledEvents.push({ key: 'on_first_layer_complete', label: t('notifications.firstLayerCompleteLabel') });
|
||||
if (onPrinterOffline) enabledEvents.push({ key: 'on_printer_offline', label: t('notifications.offline') });
|
||||
if (onPrinterError) enabledEvents.push({ key: 'on_printer_error', label: t('notifications.error') });
|
||||
if (onFilamentLow) enabledEvents.push({ key: 'on_filament_low', label: t('notifications.lowFilament') });
|
||||
if (onMaintenanceDue) enabledEvents.push({ key: 'on_maintenance_due', label: t('notifications.maintenance') });
|
||||
|
||||
if (enabledEvents.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="space-y-2 p-3 bg-bambu-dark rounded-lg">
|
||||
<p className="text-xs text-bambu-gray uppercase tracking-wide mb-1">
|
||||
{t('notifications.eventPriority.sectionTitle')}
|
||||
</p>
|
||||
<p className="text-xs text-bambu-gray mb-2">{t('notifications.eventPriority.helpNtfy')}</p>
|
||||
<div className="space-y-2">
|
||||
{enabledEvents.map((ev) => (
|
||||
<div key={ev.key} className="flex items-center justify-between gap-3">
|
||||
<span className="text-sm text-white">{ev.label}</span>
|
||||
<select
|
||||
value={eventPriorities[ev.key] ?? 3}
|
||||
onChange={(e) => {
|
||||
const next = Number(e.target.value);
|
||||
setEventPriorities((prev) => ({ ...prev, [ev.key]: next }));
|
||||
}}
|
||||
className="px-2 py-1 bg-bambu-dark-secondary border border-bambu-dark-tertiary rounded text-sm text-white focus:border-bambu-green focus:outline-none"
|
||||
>
|
||||
<option value={1}>{t('notifications.eventPriority.min')}</option>
|
||||
<option value={2}>{t('notifications.eventPriority.low')}</option>
|
||||
<option value={3}>{t('notifications.eventPriority.default')}</option>
|
||||
<option value={4}>{t('notifications.eventPriority.high')}</option>
|
||||
<option value={5}>{t('notifications.eventPriority.urgent')}</option>
|
||||
</select>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
|
||||
Reference in New Issue
Block a user