Compare commits

..

2 Commits

Author SHA1 Message Date
cpu
675c0a2d87 beter variable names 2025-03-28 21:06:41 +01:00
cpu
1a241e5355 clean up 2025-03-28 20:46:11 +01:00
5 changed files with 44 additions and 22 deletions

View File

@@ -6,9 +6,11 @@ Dockerfile
.gitignore .gitignore
README.md README.md
*.example *.example
images
.env .env
env env
labels labels
subscriptions.json subscriptions.json

View File

@@ -4,7 +4,7 @@
# Generate using: npx web-push generate-vapid-keys # Generate using: npx web-push generate-vapid-keys
VAPID_PUBLIC_KEY= VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY= VAPID_PRIVATE_KEY=
VAPID_SUBJECT=mailto:mailto:admin@virtonline.eu VAPID_SUBJECT=mailto:mailto:user@example.org
# --- Server Configuration --- # --- Server Configuration ---
# Internal port for the Node.js app # Internal port for the Node.js app
@@ -32,9 +32,11 @@ ALLOWED_HEADERS=Content-Type,Authorization
# --- Web Push Retry Configuration (Optional) --- # --- Web Push Retry Configuration (Optional) ---
# Number of retries on failure (e.g., DNS issues) # Number of retries on failure (e.g., DNS issues)
MAX_NOTIFICATION_RETRIES=3 NOTIFICATION_MAX_RETRIES=3
# Initial delay in milliseconds # First retry delay in milliseconds (minimal delay for immediate retry)
INITIAL_RETRY_DELAY_MS=1000 NOTIFICATION_FIRST_RETRY_DELAY_MS=10
# Base delay in milliseconds for subsequent retries (used for exponential backoff)
NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS=1000
# --- Network Configuration (Optional) --- # --- Network Configuration (Optional) ---
# Timeout for DNS lookups (ms) # Timeout for DNS lookups (ms)

View File

@@ -68,8 +68,9 @@ It's designed to be run as a Docker container and integrated with Traefik v3 for
* `ALLOWED_ORIGINS`: Comma-separated list of domains allowed by CORS. Include your PWA's domain if it needs to interact directly (e.g., for setup). Example: `https://my-pwa.com`. * `ALLOWED_ORIGINS`: Comma-separated list of domains allowed by CORS. Include your PWA's domain if it needs to interact directly (e.g., for setup). Example: `https://my-pwa.com`.
* `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed. * `ALLOWED_METHODS`: (Default: `POST,OPTIONS`) Standard methods needed.
* `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed. * `ALLOWED_HEADERS`: (Default: `Content-Type,Authorization`) Standard headers needed.
* `MAX_NOTIFICATION_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number. * `NOTIFICATION_MAX_RETRIES`: (Default: `3`) Number of retry attempts for failed push notifications. Must be a number.
* `INITIAL_RETRY_DELAY_MS`: (Default: `1000`) Initial delay in milliseconds before first retry. Must be a number. * `NOTIFICATION_FIRST_RETRY_DELAY_MS`: (Default: `10`) Delay in milliseconds for the first retry attempt. Setting to 0-10ms provides near-immediate first retry for transient DNS issues. Must be a number.
* `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`: (Default: `1000`) Base delay in milliseconds for subsequent retries. Each additional retry uses this value with exponential backoff and jitter. Must be a number.
* `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number. * `DNS_TIMEOUT_MS`: (Default: `5000`) DNS resolution timeout in milliseconds. Must be a number.
* `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number. * `HTTP_TIMEOUT_MS`: (Default: `10000`) HTTP request timeout in milliseconds. Must be a number.
* `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages. * `LOG_LEVEL`: (Default: `info`) Controls verbosity of logs. Valid values are `error`, `warn`, `info`, or `debug`. Use `debug` to see detailed header information and other diagnostic messages.
@@ -245,4 +246,10 @@ If you receive a different response, refer to the Troubleshooting section below.
* **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup. * **Verify `labels`:** Double-check that variables were correctly substituted manually and match your `.env` and Traefik setup.
* **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials). * **Verify `subscriptions.json`:** Ensure it's valid JSON and the button serial number (key) matches exactly what Flic sends (check backend logs for "Received webhook: Button=..."). Check if the subscription details are correct. Case sensitivity matters for the JSON keys (button serials).
* **Check Flic Configuration:** Ensure the URL, Method, `click_type` parameter, and authentication details (Username/Password if enabled) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first. * **Check Flic Configuration:** Ensure the URL, Method, `click_type` parameter, and authentication details (Username/Password if enabled) are correct in the Flic action setup. Use `curl` or Postman to test the endpoint manually first.
* **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`. * **PWA Service Worker:** Remember that the PWA needs a correctly registered Service Worker to receive and handle the incoming push messages. Ensure the PWA subscribes using the *same* `VAPID_PUBLIC_KEY` configured in the backend's `.env`.
* **Push Notification Retry Mechanism:** The service includes an optimized retry mechanism for handling temporary DNS resolution issues:
* First retry happens immediately or with minimal delay (controlled by `NOTIFICATION_FIRST_RETRY_DELAY_MS`, default 10ms)
* Subsequent retries use exponential backoff with jitter (starting from `NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS`, default 1000ms)
* Maximum number of retries is controlled by `NOTIFICATION_MAX_RETRIES` (default 3)
* This approach minimizes latency for transient DNS issues while preventing excessive requests for persistent problems
* Adjust these values in your `.env` file based on your network conditions and reliability requirements

View File

@@ -24,8 +24,9 @@ const allowedOrigins = (process.env.ALLOWED_ORIGINS || "").split(',').map(origin
const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method); const allowedMethods = (process.env.ALLOWED_METHODS || "POST,OPTIONS,GET").split(',').map(method => method.trim()).filter(method => method);
const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header); const allowedHeaders = (process.env.ALLOWED_HEADERS || "Content-Type,Authorization").split(',').map(header => header.trim()).filter(header => header);
// Retry configuration for DNS resolution issues // Retry configuration for DNS resolution issues
const maxRetries = parseInt(process.env.MAX_NOTIFICATION_RETRIES || 3, 10); const maxRetries = parseInt(process.env.NOTIFICATION_MAX_RETRIES || 3, 10);
const initialRetryDelay = parseInt(process.env.INITIAL_RETRY_DELAY_MS || 1000, 10); // 1 second const subsequentRetryDelay = parseInt(process.env.NOTIFICATION_SUBSEQUENT_RETRY_DELAY_MS || 1000, 10); // 1 second base delay for subsequent retries
const firstRetryDelay = parseInt(process.env.NOTIFICATION_FIRST_RETRY_DELAY_MS || 10, 10); // 10 milliseconds - minimal delay for first retry
// HTTP request timeout configuration // HTTP request timeout configuration
const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds const httpTimeout = parseInt(process.env.HTTP_TIMEOUT_MS || 10000, 10); // 10 seconds
// Logging level configuration // Logging level configuration
@@ -98,7 +99,7 @@ dns.setServers(dns.getServers()); // Reset DNS servers (can help in some Docker
// Example: dns.setServers(['8.8.8.8', '1.1.1.1']); // Example: dns.setServers(['8.8.8.8', '1.1.1.1']);
// --- Utility function for retrying web push notifications with exponential backoff --- // --- Utility function for retrying web push notifications with exponential backoff ---
async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = initialRetryDelay) { async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay = subsequentRetryDelay) {
try { try {
return await webpush.sendNotification(subscription, payload); return await webpush.sendNotification(subscription, payload);
} catch (error) { } catch (error) {
@@ -108,13 +109,25 @@ async function sendWebPushWithRetry(subscription, payload, retryCount = 0, delay
error.code === 'ETIMEDOUT'; error.code === 'ETIMEDOUT';
if (isDnsError && retryCount < maxRetries) { if (isDnsError && retryCount < maxRetries) {
console.log(`DNS resolution failed (${error.code}). Retrying notification in ${delay}ms (attempt ${retryCount + 1}/${maxRetries})...`); // For first retry (retryCount = 0), use minimal delay or no delay
const actualDelay = retryCount === 0 ? firstRetryDelay : delay;
// Wait for the delay if (retryCount === 0) {
await new Promise(resolve => setTimeout(resolve, delay)); logger.info(`DNS resolution failed (${error.code}). Retrying notification immediately or with minimal delay of ${firstRetryDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
} else {
logger.info(`DNS resolution failed (${error.code}). Retrying notification in ${actualDelay}ms (attempt ${retryCount + 1}/${maxRetries})...`);
}
// Exponential backoff with jitter // Wait for the delay (minimal or none for first retry)
const nextDelay = delay * (1.5 + Math.random() * 0.5); if (actualDelay > 0) {
await new Promise(resolve => setTimeout(resolve, actualDelay));
}
// Calculate next delay with exponential backoff + jitter
// First retry uses subsequentRetryDelay, subsequent retries use exponential increase
const nextDelay = retryCount === 0 ?
subsequentRetryDelay :
delay * (1.5 + Math.random() * 0.5);
// Retry recursively with increased count and delay // Retry recursively with increased count and delay
return sendWebPushWithRetry(subscription, payload, retryCount + 1, nextDelay); return sendWebPushWithRetry(subscription, payload, retryCount + 1, nextDelay);
@@ -360,7 +373,7 @@ server.listen(port, () => {
} }
logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`); logger.info(`Subscription Endpoint Auth: ${basicAuthUsername && basicAuthPassword ? 'Enabled (Basic)' : 'Disabled'}`);
logger.info(`Subscriptions File: ${subscriptionsFilePath}`); logger.info(`Subscriptions File: ${subscriptionsFilePath}`);
logger.info(`Push Notification Retry Config: ${maxRetries} retries, ${initialRetryDelay}ms initial delay`); logger.info(`Push Notification Retry Config: ${maxRetries} retries, first retry: ${firstRetryDelay}ms, subsequent retries: ${subsequentRetryDelay}ms base delay`);
logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`); logger.info(`DNS Config: IPv4 first, timeout ${dnsTimeout}ms`);
logger.info(`HTTP Timeout: ${httpTimeout}ms`); logger.info(`HTTP Timeout: ${httpTimeout}ms`);
logger.info(`Log Level: ${LOG_LEVEL.toUpperCase()}`); logger.info(`Log Level: ${LOG_LEVEL.toUpperCase()}`);

View File

@@ -7,20 +7,18 @@ DefaultDependencies=no
[Service] [Service]
Type=simple Type=simple
Environment="HOME=/root" Environment="HOME=/root"
Environment="APP_PATH=/virt/flic-webhook-webpush"
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true' ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true'
ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-flic-webhook-webpush 2>/dev/null || true' ExecStartPre=-/usr/bin/env sh -c '/usr/bin/env docker rm virt-flic-webhook-webpush 2>/dev/null || true'
ExecStartPre=/usr/bin/env sh -c 'touch ${APP_PATH}/subscriptions.json' ExecStartPre=/usr/bin/env sh -c 'touch /virt/flic-webhook-webpush/subscriptions.json'
ExecStart=/usr/bin/env docker run \ ExecStart=/usr/bin/env docker run \
--rm \ --rm \
--name=virt-flic-webhook-webpush \ --name=virt-flic-webhook-webpush \
--log-driver=none \ --log-driver=none \
--network=traefik \ --network=traefik \
--env-file=${APP_PATH}/env \ --env-file=/virt/flic-webhook-webpush/env \
--label-file=${APP_PATH}/labels \ --label-file=/virt/flic-webhook-webpush/labels \
--mount type=bind,src=${APP_PATH}/subscriptions.json,dst=/app/subscriptions.json \ --mount type=bind,src=/virt/flic-webhook-webpush/subscriptions.json,dst=/app/subscriptions.json \
flic-webhook-webpush flic-webhook-webpush
ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true' ExecStop=-/usr/bin/env sh -c '/usr/bin/env docker kill virt-flic-webhook-webpush 2>/dev/null || true'