diff --git a/configs/general/general_notifications b/configs/general/general_notifications new file mode 100644 index 0000000..20a8df0 --- /dev/null +++ b/configs/general/general_notifications @@ -0,0 +1,24 @@ +# ================================================================================ +# Notifications - Alerts when background work fails (updates, backups, installs) +# @icon 🔔 +# ================================================================================ +CFG_NOTIFY_EVENTS=failures # What to send - Failures only is the quiet default: you hear about a failed update/backup/install and nothing else. All also announces successful task results. [failures:Failures only|all:All task results|off:Off] +CFG_NOTIFY_EMAIL=false # 📧 Email - Send alerts by email. Uses the Mail settings above (CFG_MAIL_*) for the server and sender; enable those first. +CFG_NOTIFY_EMAIL_TO= # Email To - Recipient address for alert emails +CFG_NOTIFY_NTFY=false # 🔔 Ntfy - Send alerts to an ntfy topic (self-hosted or ntfy.sh) +CFG_NOTIFY_NTFY_URL=https://ntfy.sh # Ntfy Server URL - Base URL of the ntfy server +CFG_NOTIFY_NTFY_TOPIC= # Ntfy Topic - Topic name to publish alerts to +CFG_NOTIFY_NTFY_TOKEN= # Ntfy Access Token - Optional bearer token for protected topics +CFG_NOTIFY_GOTIFY=false # 🔔 Gotify - Send alerts to a self-hosted Gotify server +CFG_NOTIFY_GOTIFY_URL= # Gotify Server URL - e.g. https://gotify.example.com +CFG_NOTIFY_GOTIFY_TOKEN= # Gotify App Token - Application token from the Gotify UI +CFG_NOTIFY_DISCORD=false # 💬 Discord - Send alerts to a Discord channel via webhook +CFG_NOTIFY_DISCORD_WEBHOOK= # Discord Webhook URL - https://discord.com/api/webhooks/... +CFG_NOTIFY_SLACK=false # 💬 Slack - Send alerts to a Slack channel via incoming webhook +CFG_NOTIFY_SLACK_WEBHOOK= # Slack Webhook URL - https://hooks.slack.com/services/... +CFG_NOTIFY_TELEGRAM=false # 📱 Telegram - Send alerts via a Telegram bot +CFG_NOTIFY_TELEGRAM_TOKEN= # Telegram Bot Token - From @BotFather, e.g. 123456:ABC-DEF... +CFG_NOTIFY_TELEGRAM_CHAT_ID= # Telegram Chat ID - The chat/channel the bot posts to +CFG_NOTIFY_PUSHOVER=false # 🔔 Pushover - Send alerts via Pushover +CFG_NOTIFY_PUSHOVER_TOKEN= # Pushover App Token - Application API token +CFG_NOTIFY_PUSHOVER_USER= # Pushover User Key - Your user (or group) key diff --git a/scripts/cli/commands/notify/cli_notify_commands.sh b/scripts/cli/commands/notify/cli_notify_commands.sh new file mode 100644 index 0000000..802419f --- /dev/null +++ b/scripts/cli/commands/notify/cli_notify_commands.sh @@ -0,0 +1,227 @@ +#!/bin/bash + +# Notifications — `libreportal notify ` +# --------------------------------------------------------------------------- +# The outbound half of "hands-off background work": when a task the user is +# not watching fails (an automatic update, a scheduled backup), this is what +# tells them, on channels configured in configs/general/general_notifications. +# +# send <body...> send one message now (severity: info|high) +# task <task-id> notify about a finished task, applying the +# CFG_NOTIFY_EVENTS policy (the task +# processor calls this after every task) +# test send a test message to every enabled channel +# +# Design rules: +# * Fire-and-forget, never load-bearing: a dead webhook must not fail a task +# or block the processor, so every channel send has hard curl timeouts and +# the caller backgrounds the whole invocation. +# * The POLICY lives here, not in the processor: the processor reports every +# terminal task and this command decides (per CFG_NOTIFY_EVENTS) whether +# anyone hears about it. The daemon never needs the notify config. +# * Cancelled tasks never notify — the user did that themselves. + +cliHandleNotifyCommands() +{ + local sub="$initial_command2" + + case "$sub" in + "send") + local sev="$initial_command3" + local title="$initial_command4" + local body="$initial_command5" + if [[ -z "$title" ]]; then isError "Usage: libreportal notify send <info|high> <title> <body>"; return 1; fi + lpNotifySend "${sev:-info}" "$title" "$body" + ;; + "task") + lpNotifyTaskResult "$initial_command3" + ;; + "test") + isHeader "Sending a test notification" + if ! lpNotifyAnyChannelEnabled; then + isNotice "No notification channels are enabled — turn one on under Settings → Notifications first." + return 1 + fi + lpNotifySend "info" "LibrePortal test notification" \ + "Notifications are working on $(hostname 2>/dev/null || echo this-host). Sent $(date '+%Y-%m-%d %H:%M')." + ;; + *) + cliShowNotifyHelp + ;; + esac +} + +# True when at least one channel checkbox is on. +lpNotifyAnyChannelEnabled() +{ + [[ "$CFG_NOTIFY_EMAIL" == "true" || "$CFG_NOTIFY_NTFY" == "true" \ + || "$CFG_NOTIFY_GOTIFY" == "true" || "$CFG_NOTIFY_DISCORD" == "true" \ + || "$CFG_NOTIFY_SLACK" == "true" || "$CFG_NOTIFY_TELEGRAM" == "true" \ + || "$CFG_NOTIFY_PUSHOVER" == "true" ]] +} + +# JSON string escaper for the webhook payloads. jq is used when present (it +# handles every edge); this fallback covers the two characters that break a +# JSON string plus newlines, which is sufficient for our own message text. +_lpNotifyJson() +{ + if command -v jq >/dev/null 2>&1; then + jq -rn --arg s "$1" '$s|tojson' + else + local s="${1//\\/\\\\}"; s="${s//\"/\\\"}"; s="${s//$'\n'/\\n}" + printf '"%s"' "$s" + fi +} + +# Send one message to every enabled channel. $1 severity (info|high), $2 title, +# $3 body. Reports per-channel results; returns 0 when every enabled channel +# accepted the message (still 0 when none are enabled — nothing to fail). +lpNotifySend() +{ + local sev="$1" title="$2" body="$3" + local curl_opts=(-sS --connect-timeout 5 --max-time 8 -o /dev/null -w '%{http_code}') + local failures=0 sent=0 + + if ! command -v curl >/dev/null 2>&1; then + isError "notify: curl is required to send notifications." + return 1 + fi + + # One channel = one _try call: runs the curl, classifies the HTTP code, + # logs per-channel. 2xx = delivered; anything else (or curl error) = fail. + _try() { + local name="$1"; shift + local code; code="$(curl "${curl_opts[@]}" "$@" 2>/dev/null)" + if [[ "$code" =~ ^2[0-9][0-9]$ ]]; then + isSuccessful "notify: $name ok" + sent=$((sent + 1)) + else + isError "notify: $name failed (HTTP ${code:-none})" + failures=$((failures + 1)) + fi + } + + if [[ "$CFG_NOTIFY_NTFY" == "true" && -n "$CFG_NOTIFY_NTFY_TOPIC" ]]; then + local ntfy_args=(-H "Title: $title" -H "Priority: $([[ "$sev" == "high" ]] && echo high || echo default)") + [[ -n "$CFG_NOTIFY_NTFY_TOKEN" ]] && ntfy_args+=(-H "Authorization: Bearer $CFG_NOTIFY_NTFY_TOKEN") + _try ntfy "${ntfy_args[@]}" -d "$body" "${CFG_NOTIFY_NTFY_URL%/}/$CFG_NOTIFY_NTFY_TOPIC" + fi + + if [[ "$CFG_NOTIFY_GOTIFY" == "true" && -n "$CFG_NOTIFY_GOTIFY_URL" && -n "$CFG_NOTIFY_GOTIFY_TOKEN" ]]; then + _try gotify -F "title=$title" -F "message=$body" \ + -F "priority=$([[ "$sev" == "high" ]] && echo 8 || echo 4)" \ + "${CFG_NOTIFY_GOTIFY_URL%/}/message?token=$CFG_NOTIFY_GOTIFY_TOKEN" + fi + + if [[ "$CFG_NOTIFY_DISCORD" == "true" && -n "$CFG_NOTIFY_DISCORD_WEBHOOK" ]]; then + _try discord -H 'Content-Type: application/json' \ + -d "{\"content\": $(_lpNotifyJson "**${title}**"$'\n'"${body}")}" \ + "$CFG_NOTIFY_DISCORD_WEBHOOK" + fi + + if [[ "$CFG_NOTIFY_SLACK" == "true" && -n "$CFG_NOTIFY_SLACK_WEBHOOK" ]]; then + _try slack -H 'Content-Type: application/json' \ + -d "{\"text\": $(_lpNotifyJson "*${title}*"$'\n'"${body}")}" \ + "$CFG_NOTIFY_SLACK_WEBHOOK" + fi + + if [[ "$CFG_NOTIFY_TELEGRAM" == "true" && -n "$CFG_NOTIFY_TELEGRAM_TOKEN" && -n "$CFG_NOTIFY_TELEGRAM_CHAT_ID" ]]; then + _try telegram --data-urlencode "chat_id=$CFG_NOTIFY_TELEGRAM_CHAT_ID" \ + --data-urlencode "text=${title}"$'\n'"${body}" \ + "https://api.telegram.org/bot${CFG_NOTIFY_TELEGRAM_TOKEN}/sendMessage" + fi + + if [[ "$CFG_NOTIFY_PUSHOVER" == "true" && -n "$CFG_NOTIFY_PUSHOVER_TOKEN" && -n "$CFG_NOTIFY_PUSHOVER_USER" ]]; then + _try pushover -F "token=$CFG_NOTIFY_PUSHOVER_TOKEN" -F "user=$CFG_NOTIFY_PUSHOVER_USER" \ + -F "title=$title" -F "message=$body" \ + -F "priority=$([[ "$sev" == "high" ]] && echo 1 || echo 0)" \ + "https://api.pushover.net/1/messages.json" + fi + + # Email rides the existing Mail settings (host/creds/from) — the notify + # config only adds the on/off switch and the recipient. curl speaks SMTP + # natively: smtps:// for implicit TLS (465), --ssl-reqd for STARTTLS (587). + if [[ "$CFG_NOTIFY_EMAIL" == "true" && -n "$CFG_NOTIFY_EMAIL_TO" ]]; then + if [[ "$CFG_MAIL_ENABLED" != "true" ]]; then + isError "notify: email is enabled but Mail settings are off (CFG_MAIL_ENABLED) — skipping." + failures=$((failures + 1)) + else + local proto="smtp" mail_args=(--mail-from "$CFG_MAIL_FROM" --mail-rcpt "$CFG_NOTIFY_EMAIL_TO") + case "$CFG_MAIL_SECURE" in + ssl) proto="smtps" ;; + tls) mail_args+=(--ssl-reqd) ;; + esac + [[ -n "$CFG_MAIL_USERNAME" ]] && mail_args+=(--user "$CFG_MAIL_USERNAME:$CFG_MAIL_PASSWORD") + local msg; msg="$(printf 'From: LibrePortal <%s>\r\nTo: %s\r\nSubject: %s\r\n\r\n%s\r\n' \ + "$CFG_MAIL_FROM" "$CFG_NOTIFY_EMAIL_TO" "$title" "$body")" + local ecode; ecode="$(printf '%s' "$msg" | curl -sS --connect-timeout 5 --max-time 15 \ + -o /dev/null -w '%{http_code}' "${mail_args[@]}" -T - \ + "${proto}://${CFG_MAIL_HOST}:${CFG_MAIL_PORT}" 2>/dev/null; echo "rc=$?")" + # SMTP via curl reports no meaningful http_code; trust the exit code. + if [[ "$ecode" == *"rc=0"* ]]; then + isSuccessful "notify: email ok" + sent=$((sent + 1)) + else + isError "notify: email failed (${ecode##*rc=})" + failures=$((failures + 1)) + fi + fi + fi + + if (( sent == 0 && failures == 0 )); then + isNotice "notify: no channels enabled — nothing sent." + return 0 + fi + (( failures == 0 )) +} + +# Notify about one finished task, applying the events policy. Called by the +# task processor after EVERY terminal task; this is where "should anyone hear +# about it" is decided, so the daemon stays policy-free. +lpNotifyTaskResult() +{ + local task_id="$1" + [[ "$task_id" =~ ^task_[0-9]+_[A-Za-z0-9]+$ ]] || return 0 + local events="${CFG_NOTIFY_EVENTS:-failures}" + [[ "$events" == "off" ]] && return 0 + lpNotifyAnyChannelEnabled || return 0 + + local f="${containers_dir%/}/libreportal/frontend/data/tasks/${task_id}.json" + [[ -f "$f" ]] || return 0 + local status type app exit_code + if command -v jq >/dev/null 2>&1; then + read -r status type app exit_code < <(jq -r '[.status, .type // "task", .app // "", (.exit_code // 0 | tostring)] | join(" ")' "$f" 2>/dev/null) + else + status=$(grep -oE '"status"[[:space:]]*:[[:space:]]*"[^"]+"' "$f" | head -1 | sed 's/.*"\([^"]*\)"$/\1/') + type=$(grep -oE '"type"[[:space:]]*:[[:space:]]*"[^"]+"' "$f" | head -1 | sed 's/.*"\([^"]*\)"$/\1/') + app=$(grep -oE '"app"[[:space:]]*:[[:space:]]*"[^"]+"' "$f" | head -1 | sed 's/.*"\([^"]*\)"$/\1/') + exit_code="" + fi + [[ "$status" == "cancelled" ]] && return 0 # the user did that + [[ "$status" == "failed" || "$events" == "all" ]] || return 0 + + # Human copy per task type — the failure lines say what ALREADY happened + # (rollback, retained snapshot), so the reader knows the box is safe before + # they even open the WebUI. + local host; host="$(hostname 2>/dev/null || echo LibrePortal)" + local label="${type:-task}"; local extra="" + case "$type" in + updater_apply|updater_apply_all) + label="Update${app:+ of $app}" + extra=" The app was rolled back to its pre-update snapshot and this build will not be retried automatically — retry from the Updates page when ready." ;; + updater_rollback) label="Rollback${app:+ of $app}" ;; + backup) label="Backup${app:+ of $app}" + extra=" Check the Backups page — the previous snapshots are unaffected." ;; + restore) label="Restore${app:+ of $app}" ;; + install) label="Install${app:+ of $app}" ;; + *) label="Task ${type:-?}${app:+ ($app)}" ;; + esac + + if [[ "$status" == "failed" ]]; then + lpNotifySend "high" "$host: ${label} failed" \ + "${label} failed (exit ${exit_code:-?}) at $(date '+%H:%M').${extra} Full log: WebUI → Tasks → ${task_id}." + else + lpNotifySend "info" "$host: ${label} completed" \ + "${label} finished successfully at $(date '+%H:%M')." + fi +} diff --git a/scripts/cli/commands/notify/cli_notify_header.sh b/scripts/cli/commands/notify/cli_notify_header.sh new file mode 100644 index 0000000..e549755 --- /dev/null +++ b/scripts/cli/commands/notify/cli_notify_header.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Notification Commands Header +# Shows available `libreportal notify` subcommands. + +cliShowNotifyHelp() +{ + echo "" + echo "Available Notification Commands:" + echo "" + echo " libreportal notify test - Send a test message to every enabled channel" + echo " libreportal notify send <sev> <title> <body> - Send one message now (sev: info|high)" + echo " libreportal notify task <task-id> - Notify about a finished task (used by the task processor)" + echo "" + echo "Channels (email, ntfy, Gotify, Discord, Slack, Telegram, Pushover) are" + echo "configured under Settings → Notifications. The default policy notifies" + echo "on FAILED background tasks only — a failed automatic update or backup" + echo "reaches you without the WebUI being open. Cancelled tasks never notify." + echo "" +} diff --git a/scripts/source/files/arrays/files_cli.sh b/scripts/source/files/arrays/files_cli.sh index 1ad8326..616ffae 100755 --- a/scripts/source/files/arrays/files_cli.sh +++ b/scripts/source/files/arrays/files_cli.sh @@ -33,6 +33,8 @@ cli_scripts=( "cli/commands/instance/cli_instance_header.sh" "cli/commands/ip/cli_ip_commands.sh" "cli/commands/ip/cli_ip_header.sh" + "cli/commands/notify/cli_notify_commands.sh" + "cli/commands/notify/cli_notify_header.sh" "cli/commands/peer/cli_peer_commands.sh" "cli/commands/peer/cli_peer_header.sh" "cli/commands/regen/cli_regen_commands.sh" diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index 1818222..6153f26 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -297,6 +297,7 @@ declare -gA LP_FN_MAP=( [cliHandleInstallCommands]="cli/commands/install/cli_install_commands.sh" [cliHandleInstanceCommands]="cli/commands/instance/cli_instance_commands.sh" [cliHandleIPCommands]="cli/commands/ip/cli_ip_commands.sh" + [cliHandleNotifyCommands]="cli/commands/notify/cli_notify_commands.sh" [cliHandlePeerCommands]="cli/commands/peer/cli_peer_commands.sh" [cliHandleRegenCommands]="cli/commands/regen/cli_regen_commands.sh" [cliHandleResetCommands]="cli/commands/reset/cli_reset_commands.sh" @@ -322,6 +323,7 @@ declare -gA LP_FN_MAP=( [cliShowInstallHelp]="cli/commands/install/cli_install_header.sh" [cliShowInstanceHelp]="cli/commands/instance/cli_instance_header.sh" [cliShowIPHelp]="cli/commands/ip/cli_ip_header.sh" + [cliShowNotifyHelp]="cli/commands/notify/cli_notify_header.sh" [cliShowPeerHelp]="cli/commands/peer/cli_peer_header.sh" [cliShowRegenHelp]="cli/commands/regen/cli_regen_header.sh" [cliShowResetHelp]="cli/commands/reset/cli_reset_header.sh" @@ -631,6 +633,10 @@ declare -gA LP_FN_MAP=( [_lpJsonEsc]="source/fetch.sh" [_lpJsonNum]="source/fetch.sh" [_lpJsonStr]="source/fetch.sh" + [lpNotifyAnyChannelEnabled]="cli/commands/notify/cli_notify_commands.sh" + [_lpNotifyJson]="cli/commands/notify/cli_notify_commands.sh" + [lpNotifySend]="cli/commands/notify/cli_notify_commands.sh" + [lpNotifyTaskResult]="cli/commands/notify/cli_notify_commands.sh" [lpRegen]="webui/webui_regen.sh" [lpRegenArrays]="webui/webui_regen.sh" [_lpRegenStale]="webui/webui_regen.sh" @@ -1311,6 +1317,7 @@ declare -gA LP_FN_ROOT=( [cliHandleInstallCommands]="scripts" [cliHandleInstanceCommands]="scripts" [cliHandleIPCommands]="scripts" + [cliHandleNotifyCommands]="scripts" [cliHandlePeerCommands]="scripts" [cliHandleRegenCommands]="scripts" [cliHandleResetCommands]="scripts" @@ -1336,6 +1343,7 @@ declare -gA LP_FN_ROOT=( [cliShowInstallHelp]="scripts" [cliShowInstanceHelp]="scripts" [cliShowIPHelp]="scripts" + [cliShowNotifyHelp]="scripts" [cliShowPeerHelp]="scripts" [cliShowRegenHelp]="scripts" [cliShowResetHelp]="scripts" @@ -1645,6 +1653,10 @@ declare -gA LP_FN_ROOT=( [_lpJsonEsc]="scripts" [_lpJsonNum]="scripts" [_lpJsonStr]="scripts" + [lpNotifyAnyChannelEnabled]="scripts" + [_lpNotifyJson]="scripts" + [lpNotifySend]="scripts" + [lpNotifyTaskResult]="scripts" [lpRegen]="scripts" [lpRegenArrays]="scripts" [_lpRegenStale]="scripts" @@ -2358,6 +2370,7 @@ cliHandleHelpCommands() { unset -f cliHandleHelpCommands; __lpAutoload "${instal cliHandleInstallCommands() { unset -f cliHandleInstallCommands; __lpAutoload "${install_scripts_dir}cli/commands/install/cli_install_commands.sh"; cliHandleInstallCommands "$@"; } cliHandleInstanceCommands() { unset -f cliHandleInstanceCommands; __lpAutoload "${install_scripts_dir}cli/commands/instance/cli_instance_commands.sh"; cliHandleInstanceCommands "$@"; } cliHandleIPCommands() { unset -f cliHandleIPCommands; __lpAutoload "${install_scripts_dir}cli/commands/ip/cli_ip_commands.sh"; cliHandleIPCommands "$@"; } +cliHandleNotifyCommands() { unset -f cliHandleNotifyCommands; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; cliHandleNotifyCommands "$@"; } cliHandlePeerCommands() { unset -f cliHandlePeerCommands; __lpAutoload "${install_scripts_dir}cli/commands/peer/cli_peer_commands.sh"; cliHandlePeerCommands "$@"; } cliHandleRegenCommands() { unset -f cliHandleRegenCommands; __lpAutoload "${install_scripts_dir}cli/commands/regen/cli_regen_commands.sh"; cliHandleRegenCommands "$@"; } cliHandleResetCommands() { unset -f cliHandleResetCommands; __lpAutoload "${install_scripts_dir}cli/commands/reset/cli_reset_commands.sh"; cliHandleResetCommands "$@"; } @@ -2383,6 +2396,7 @@ cliShowHelpHelp() { unset -f cliShowHelpHelp; __lpAutoload "${install_scripts_di cliShowInstallHelp() { unset -f cliShowInstallHelp; __lpAutoload "${install_scripts_dir}cli/commands/install/cli_install_header.sh"; cliShowInstallHelp "$@"; } cliShowInstanceHelp() { unset -f cliShowInstanceHelp; __lpAutoload "${install_scripts_dir}cli/commands/instance/cli_instance_header.sh"; cliShowInstanceHelp "$@"; } cliShowIPHelp() { unset -f cliShowIPHelp; __lpAutoload "${install_scripts_dir}cli/commands/ip/cli_ip_header.sh"; cliShowIPHelp "$@"; } +cliShowNotifyHelp() { unset -f cliShowNotifyHelp; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_header.sh"; cliShowNotifyHelp "$@"; } cliShowPeerHelp() { unset -f cliShowPeerHelp; __lpAutoload "${install_scripts_dir}cli/commands/peer/cli_peer_header.sh"; cliShowPeerHelp "$@"; } cliShowRegenHelp() { unset -f cliShowRegenHelp; __lpAutoload "${install_scripts_dir}cli/commands/regen/cli_regen_header.sh"; cliShowRegenHelp "$@"; } cliShowResetHelp() { unset -f cliShowResetHelp; __lpAutoload "${install_scripts_dir}cli/commands/reset/cli_reset_header.sh"; cliShowResetHelp "$@"; } @@ -2692,6 +2706,10 @@ lpInstalledFootprintVersion() { unset -f lpInstalledFootprintVersion; __lpAutolo _lpJsonEsc() { unset -f _lpJsonEsc; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonEsc "$@"; } _lpJsonNum() { unset -f _lpJsonNum; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonNum "$@"; } _lpJsonStr() { unset -f _lpJsonStr; __lpAutoload "${install_scripts_dir}source/fetch.sh"; _lpJsonStr "$@"; } +lpNotifyAnyChannelEnabled() { unset -f lpNotifyAnyChannelEnabled; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; lpNotifyAnyChannelEnabled "$@"; } +_lpNotifyJson() { unset -f _lpNotifyJson; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; _lpNotifyJson "$@"; } +lpNotifySend() { unset -f lpNotifySend; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; lpNotifySend "$@"; } +lpNotifyTaskResult() { unset -f lpNotifyTaskResult; __lpAutoload "${install_scripts_dir}cli/commands/notify/cli_notify_commands.sh"; lpNotifyTaskResult "$@"; } lpRegen() { unset -f lpRegen; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; lpRegen "$@"; } lpRegenArrays() { unset -f lpRegenArrays; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; lpRegenArrays "$@"; } _lpRegenStale() { unset -f _lpRegenStale; __lpAutoload "${install_scripts_dir}webui/webui_regen.sh"; _lpRegenStale "$@"; } diff --git a/scripts/task/crontab_task_processor.sh b/scripts/task/crontab_task_processor.sh index e1393fb..26ebcd0 100755 --- a/scripts/task/crontab_task_processor.sh +++ b/scripts/task/crontab_task_processor.sh @@ -429,6 +429,15 @@ runTask() { cancelled) logInfo "Task $taskId cancelled";; failed) logError "Task $taskId failed (exit $rc)";; esac + + # Outbound notification for the finished task. The POLICY (which events, which + # channels, or nothing at all) lives entirely in `notify task` — the daemon + # just reports every terminal task and moves on. Detached + backgrounded so a + # slow webhook can never block the dispatch loop, and failures of the notify + # itself are deliberately ignored: notifications are never load-bearing. + if command -v libreportal >/dev/null 2>&1; then + ( libreportal notify task "$taskId" >/dev/null 2>&1 & ) 2>/dev/null + fi } # ============================================================================