node-red-contrib-wger
Version:
Node-RED nodes for integrating with wger workout and fitness tracker API
385 lines (338 loc) • 16.4 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('wger-config', {
category: 'config',
defaults: {
name: { value: "" },
apiUrl: { value: "https://wger.de", required: true },
authType: { value: "none", required: true },
allowPrivateHosts: { value: false },
// Retry configuration
enableRetry: { value: false },
retryMaxAttempts: { value: 3, validate: RED.validators.number() },
retryBaseDelayMs: { value: 1000, validate: RED.validators.number() },
retryMaxDelayMs: { value: 30000, validate: RED.validators.number() },
// Circuit breaker configuration
enableCircuitBreaker: { value: false },
circuitBreakerFailureThreshold: { value: 5, validate: RED.validators.number() },
circuitBreakerResetTimeoutMs: { value: 60000, validate: RED.validators.number() }
},
credentials: {
token: { type: "password" },
username: { type: "text" },
password: { type: "password" }
},
label: function () {
return this.name || "wger server";
},
oneditprepare: function () {
const node = this;
// Authentication type change handler
$("#node-config-input-authType").on('change', function () {
const selectedAuth = $(this).val();
if (selectedAuth === "token" || selectedAuth === "jwt") {
$("#node-config-row-token").show();
$("#node-config-row-credentials").hide();
} else if (selectedAuth === "basic") {
$("#node-config-row-token").hide();
$("#node-config-row-credentials").show();
} else {
$("#node-config-row-token").hide();
$("#node-config-row-credentials").hide();
}
});
// Retry configuration toggle handler
$("#node-config-input-enableRetry").on('change', function () {
if ($(this).is(':checked')) {
$("#node-config-retry-options").show();
} else {
$("#node-config-retry-options").hide();
}
});
// Circuit breaker configuration toggle handler
$("#node-config-input-enableCircuitBreaker").on('change', function () {
if ($(this).is(':checked')) {
$("#node-config-circuit-breaker-options").show();
} else {
$("#node-config-circuit-breaker-options").hide();
}
});
// Collapsible sections handler
$(".node-config-section-header").on('click', function () {
const $header = $(this);
const $content = $header.next('.node-config-section-content');
const $icon = $header.find('.fa');
if ($content.is(':visible')) {
$content.slideUp();
$icon.removeClass('fa-chevron-down').addClass('fa-chevron-right');
} else {
$content.slideDown();
$icon.removeClass('fa-chevron-right').addClass('fa-chevron-down');
}
});
// Trigger initial state
$("#node-config-input-authType").trigger('change');
$("#node-config-input-enableRetry").trigger('change');
$("#node-config-input-enableCircuitBreaker").trigger('change');
// API URL validation
$("#node-config-input-apiUrl").on('change', function () {
let url = $(this).val();
if (!url.startsWith("http://") && !url.startsWith("https://")) {
$(this).val("https://" + url);
}
// Remove trailing slash if present
if (url.endsWith("/")) {
$(this).val(url.slice(0, -1));
}
});
// Test connection button
$("#node-config-test-connection").on('click', function () {
const node_id = $("#node-config-dialog").attr('node-id');
const apiUrl = $("#node-config-input-apiUrl").val();
const authType = $("#node-config-input-authType").val();
// Note: Credentials are NOT sent from client for security
// Server will use stored credentials via RED.nodes.getCredentials()
// Show loading state
const $button = $(this);
const $status = $("#node-config-test-connection-status");
const $statusMessage = $("#node-config-test-connection-message");
$button.prop('disabled', true);
$status.removeClass('success error').addClass('loading');
$statusMessage.text('Testing connection...');
// Make test request - only send non-sensitive configuration
const allowPrivateHosts = $("#node-config-input-allowPrivateHosts").is(':checked');
$.ajax({
url: `wger-config/${node_id}/test`,
type: 'POST',
data: JSON.stringify({ apiUrl, authType, allowPrivateHosts }),
contentType: 'application/json; charset=utf-8',
success: function (response) {
$button.prop('disabled', false);
if (response.success) {
$status.removeClass('loading error').addClass('success');
$statusMessage.text('Connection successful!');
} else {
$status.removeClass('loading success').addClass('error');
$statusMessage.text(`Connection failed: ${response.message}`);
}
},
error: function (jqXHR, textStatus, errorThrown) {
$button.prop('disabled', false);
$status.removeClass('loading success').addClass('error');
$statusMessage.text(`Connection failed: ${errorThrown}`);
}
});
});
}
});
</script>
<script type="text/html" data-template-name="wger-config">
<div class="form-row">
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-config-input-name" placeholder="Name">
</div>
<div class="form-row">
<label for="node-config-input-apiUrl"><i class="fa fa-globe"></i> API URL</label>
<input type="text" id="node-config-input-apiUrl" placeholder="https://wger.de">
</div>
<div class="form-row">
<label for="node-config-input-authType"><i class="fa fa-lock"></i> Auth Type</label>
<select id="node-config-input-authType">
<option value="none">None</option>
<option value="token">Token</option>
<option value="jwt">JWT</option>
</select>
</div>
<div class="form-row" id="node-config-row-token">
<label for="node-config-input-token"><i class="fa fa-key"></i> Token</label>
<input type="password" id="node-config-input-token">
</div>
<div class="form-row" id="node-config-row-credentials" style="display: none;">
<label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
<input type="text" id="node-config-input-username">
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
<input type="password" id="node-config-input-password">
</div>
<div class="form-row">
<label for="node-config-input-allowPrivateHosts">
<input type="checkbox" id="node-config-input-allowPrivateHosts" style="width: auto; margin-right: 8px;">
<i class="fa fa-shield"></i> Allow Private Hosts
</label>
<div class="form-tips" style="margin-left: 20px; font-size: 11px; color: #666;">
Enable for self-hosted instances on private networks (localhost, 192.168.x.x, 10.x.x.x, etc.)
</div>
</div>
<!-- Resilience Configuration Section -->
<div class="form-row node-config-section-header" style="margin-top: 20px; padding: 8px 0; border-top: 1px solid #ccc; cursor: pointer;">
<i class="fa fa-chevron-right" style="margin-right: 8px;"></i>
<span style="font-weight: bold;">Resilience Configuration</span>
</div>
<div class="node-config-section-content" style="display: none; padding-left: 20px;">
<!-- Retry Configuration -->
<div class="form-row">
<label for="node-config-input-enableRetry">
<input type="checkbox" id="node-config-input-enableRetry" style="width: auto; margin-right: 8px;">
<i class="fa fa-repeat"></i> Enable Retry Policy
</label>
<div class="form-tips">Automatically retry failed requests with exponential backoff</div>
</div>
<div id="node-config-retry-options" style="display: none; margin-left: 20px;">
<div class="form-row">
<label for="node-config-input-retryMaxAttempts">Max Attempts</label>
<input type="number" id="node-config-input-retryMaxAttempts" min="1" max="10" style="width: 100px;">
<span class="form-tips" style="margin-left: 8px;">Maximum retry attempts (1-10)</span>
</div>
<div class="form-row">
<label for="node-config-input-retryBaseDelayMs">Base Delay (ms)</label>
<input type="number" id="node-config-input-retryBaseDelayMs" min="100" max="10000" step="100" style="width: 120px;">
<span class="form-tips" style="margin-left: 8px;">Initial delay between retries</span>
</div>
<div class="form-row">
<label for="node-config-input-retryMaxDelayMs">Max Delay (ms)</label>
<input type="number" id="node-config-input-retryMaxDelayMs" min="1000" max="300000" step="1000" style="width: 120px;">
<span class="form-tips" style="margin-left: 8px;">Maximum delay between retries</span>
</div>
</div>
<!-- Circuit Breaker Configuration -->
<div class="form-row" style="margin-top: 15px;">
<label for="node-config-input-enableCircuitBreaker">
<input type="checkbox" id="node-config-input-enableCircuitBreaker" style="width: auto; margin-right: 8px;">
<i class="fa fa-shield"></i> Enable Circuit Breaker
</label>
<div class="form-tips">Prevent cascading failures by temporarily blocking requests after repeated failures</div>
</div>
<div id="node-config-circuit-breaker-options" style="display: none; margin-left: 20px;">
<div class="form-row">
<label for="node-config-input-circuitBreakerFailureThreshold">Failure Threshold</label>
<input type="number" id="node-config-input-circuitBreakerFailureThreshold" min="1" max="50" style="width: 100px;">
<span class="form-tips" style="margin-left: 8px;">Consecutive failures before opening circuit</span>
</div>
<div class="form-row">
<label for="node-config-input-circuitBreakerResetTimeoutMs">Reset Timeout (ms)</label>
<input type="number" id="node-config-input-circuitBreakerResetTimeoutMs" min="5000" max="600000" step="5000" style="width: 120px;">
<span class="form-tips" style="margin-left: 8px;">Wait time before attempting to close circuit</span>
</div>
</div>
</div>
<div class="form-row" style="margin-top: 20px;">
<button type="button" id="node-config-test-connection" class="red-ui-button">Test Connection</button>
<span id="node-config-test-connection-status" class="test-status"></span>
<span id="node-config-test-connection-message" class="test-message"></span>
</div>
</script>
<script type="text/html" data-help-name="wger-config">
<p>Configuration node for connecting to a wger API instance.</p>
<h3>Configuration</h3>
<dl class="message-properties">
<dt>API URL <span class="property-type">string</span></dt>
<dd>The base URL of your wger instance (e.g., https://wger.de)</dd>
<dt>Auth Type <span class="property-type">string</span></dt>
<dd>Authentication method (None, Token, JWT)</dd>
<dt>Token <span class="property-type">string</span></dt>
<dd>Your API token (required if using Token or JWT auth)</dd>
</dl>
<h3>Resilience Configuration</h3>
<p>Advanced configuration options to improve reliability and handle API failures gracefully.</p>
<h4>Retry Policy</h4>
<dl class="message-properties">
<dt>Enable Retry Policy <span class="property-type">boolean</span></dt>
<dd>Automatically retry failed requests with exponential backoff and jitter</dd>
<dt>Max Attempts <span class="property-type">number</span></dt>
<dd>Maximum number of retry attempts (1-10, default: 3)</dd>
<dt>Base Delay <span class="property-type">number</span></dt>
<dd>Initial delay between retries in milliseconds (100-10000, default: 1000)</dd>
<dt>Max Delay <span class="property-type">number</span></dt>
<dd>Maximum delay between retries in milliseconds (1000-300000, default: 30000)</dd>
</dl>
<h4>Circuit Breaker</h4>
<dl class="message-properties">
<dt>Enable Circuit Breaker <span class="property-type">boolean</span></dt>
<dd>Prevent cascading failures by temporarily blocking requests after repeated failures</dd>
<dt>Failure Threshold <span class="property-type">number</span></dt>
<dd>Number of consecutive failures before opening the circuit (1-50, default: 5)</dd>
<dt>Reset Timeout <span class="property-type">number</span></dt>
<dd>Wait time in milliseconds before attempting to close the circuit (5000-600000, default: 60000)</dd>
</dl>
<h3>Details</h3>
<p>This node configures the connection to a wger instance. You can use the official https://wger.de server or your own self-hosted instance.</p>
<h3>Authentication</h3>
<p>The wger API supports several authentication methods:</p>
<ul>
<li><b>None</b> - For public endpoints (limited access)</li>
<li><b>Token</b> - Uses a permanent API token</li>
<li><b>JWT</b> - Uses JSON Web Tokens for authentication</li>
</ul>
<h3>Resilience Features</h3>
<p>The resilience configuration helps improve reliability when dealing with network issues or API problems:</p>
<ul>
<li><b>Retry Policy</b> - Automatically retries failed requests using exponential backoff with jitter to prevent thundering herd problems. Retries are attempted for network errors, timeouts, and specific HTTP status codes (429, 502, 503, 504).</li>
<li><b>Circuit Breaker</b> - Temporarily blocks requests when too many consecutive failures occur, giving the API time to recover. The circuit automatically attempts to close after the reset timeout period.</li>
</ul>
<p>These features are particularly useful when connecting to self-hosted wger instances or when dealing with unreliable network conditions.</p>
<h3>References</h3>
<ul>
<li><a href="https://wger.de" target="_blank">wger Official Website</a></li>
<li><a href="https://wger.readthedocs.io/en/latest/api.html" target="_blank">API Documentation</a></li>
<li><a href="https://github.com/wger-project/wger" target="_blank">GitHub Repository</a></li>
</ul>
</script>
<style>
.test-status {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-left: 10px;
}
.test-status.loading {
background-color: #ffa500;
}
.test-status.success {
background-color: #28a745;
}
.test-status.error {
background-color: #dc3545;
}
.test-message {
margin-left: 10px;
color: #666;
}
.node-config-section-header {
background-color: #f9f9f9;
border-radius: 3px;
padding: 8px 12px ;
margin: 15px 0 5px 0 ;
border-top: 1px solid #ddd ;
user-select: none;
transition: background-color 0.2s;
}
.node-config-section-header:hover {
background-color: #f0f0f0;
}
.node-config-section-header i {
color: #666;
transition: transform 0.2s;
}
.node-config-section-content {
border-left: 2px solid #e6e6e6;
margin-left: 10px;
padding-left: 15px;
background-color: #fafafa;
border-radius: 0 3px 3px 0;
padding-top: 10px;
padding-bottom: 10px;
}
.form-tips {
font-size: 11px;
color: #888;
font-style: italic;
margin-top: 2px;
line-height: 1.3;
}
.node-config-section-content .form-row {
margin-bottom: 12px;
}
.node-config-section-content input[type="number"] {
background-color: white;
border: 1px solid #ccc;
}
</style>