Build and Publish / Build and Publish Docker Image (push) Successful in 1m42s
347 lines
21 KiB
HTML
347 lines
21 KiB
HTML
{% extends "admin/layout.html" %}
|
|
{% let active_page = "settings" %}
|
|
|
|
{% block title %}{{ t.settings_title }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>{{ t.settings_title }}</h1>
|
|
<p>{{ t.settings_intro }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
{% if saved %}
|
|
<div class="notification is-success is-light admin-message">{{ t.settings_saved }}</div>
|
|
{% endif %}
|
|
{% if let Some(message) = error %}
|
|
<div class="notification is-danger is-light admin-message">{{ message }}</div>
|
|
{% endif %}
|
|
|
|
<form id="settingsForm" class="admin-form" method="post" action="/admin/settings/save">
|
|
<section class="admin-section">
|
|
<header class="admin-section-head">
|
|
<span class="admin-section-icon" aria-hidden="true">🌐</span>
|
|
<div>
|
|
<h2>{{ t.settings_section_general }}</h2>
|
|
<p>{{ t.settings_section_general_help }}</p>
|
|
</div>
|
|
</header>
|
|
<div class="admin-section-body">
|
|
<div class="admin-grid">
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="contactInfo">{{ t.settings_contact_info }}</label>
|
|
<div class="control">
|
|
<input id="contactInfo" class="input" type="text" name="contact_info" placeholder="+7 999 123-45-67 / info@example.com" value="{% for s in &settings %}{% if s.key == "contact_info" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="siteDomain">{{ t.settings_site_domain }}</label>
|
|
<div class="control">
|
|
<input id="siteDomain" class="input" type="url" name="site_domain" placeholder="https://example.com" value="{% for s in &settings %}{% if s.key == "site_domain" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="timezone">{{ t.settings_timezone }}</label>
|
|
<div class="control">
|
|
<input id="timezone" class="input" type="text" name="timezone" placeholder="Asia/Vladivostok" value="{% for s in &settings %}{% if s.key == "timezone" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="pricingInfo">{{ t.settings_pricing_info }}</label>
|
|
<div class="control">
|
|
<textarea id="pricingInfo" class="textarea" name="pricing_info" rows="3" placeholder="от 600 рублей за визит">{% for s in &settings %}{% if s.key == "pricing_info" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="seoKeywordsInput">{{ t.settings_seo_keywords }}</label>
|
|
<div class="control">
|
|
<textarea id="seoKeywordsInput" class="textarea" name="seo_keywords" rows="3" placeholder="зооняня Хабаровск, присмотр за питомцем Хабаровск, догситтер Хабаровск">{% for s in &settings %}{% if s.key == "seo_keywords" %}{{ s.value }}{% endif %}{% endfor %}</textarea>
|
|
</div>
|
|
<div id="seoPreview" class="seo-preview" hidden></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="admin-section">
|
|
<header class="admin-section-head">
|
|
<span class="admin-section-icon" aria-hidden="true">☁️</span>
|
|
<div>
|
|
<div class="admin-title-row">
|
|
<h2>{{ t.settings_section_storage }}</h2>
|
|
<span class="status-pill{% if r2_enabled_checked %} is-enabled{% endif %}">
|
|
{% if r2_enabled_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
|
</span>
|
|
</div>
|
|
<p>{{ t.settings_r2_help }}</p>
|
|
</div>
|
|
</header>
|
|
<div class="admin-section-body">
|
|
<label class="admin-toggle" for="r2Enabled">
|
|
<input id="r2Enabled" type="checkbox" name="r2_enabled" value="true"{% if r2_enabled_checked %} checked{% endif %}>
|
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
|
<span>
|
|
<strong>{{ t.settings_r2_enabled }}</strong>
|
|
<small>{{ t.settings_r2_help }}</small>
|
|
</span>
|
|
</label>
|
|
|
|
<div class="admin-note admin-note-info">{{ t.settings_r2_migration_help }}</div>
|
|
|
|
<div class="admin-grid">
|
|
<div class="field admin-field">
|
|
<label class="label" for="r2AccountId">{{ t.settings_r2_account_id }}</label>
|
|
<div class="control">
|
|
<input id="r2AccountId" class="input is-family-monospace" type="text" name="r2_account_id" maxlength="32" autocomplete="off" spellcheck="false" placeholder="0123456789abcdef0123456789abcdef" value="{% for s in &settings %}{% if s.key == "r2_account_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="r2Bucket">{{ t.settings_r2_bucket }}</label>
|
|
<div class="control">
|
|
<input id="r2Bucket" class="input is-family-monospace" type="text" name="r2_bucket" autocomplete="off" spellcheck="false" placeholder="pet-media" value="{% for s in &settings %}{% if s.key == "r2_bucket" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="r2AccessKey">{{ t.settings_r2_access_key_id }}</label>
|
|
<div class="control">
|
|
<input id="r2AccessKey" class="input is-family-monospace" type="text" name="r2_access_key_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "r2_access_key_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="r2Secret">{{ t.settings_r2_secret_access_key }}</label>
|
|
<div class="control secret-control">
|
|
<input id="r2Secret" class="input is-family-monospace" type="password" name="r2_secret_access_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
|
</div>
|
|
<p class="field-help">{% if r2_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
|
{% if r2_secret_configured %}
|
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_r2_secret_access_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="admin-section">
|
|
<header class="admin-section-head">
|
|
<span class="admin-section-icon" aria-hidden="true">🔔</span>
|
|
<div>
|
|
<div class="admin-title-row">
|
|
<h2>{{ t.settings_section_notifications }}</h2>
|
|
<span class="status-pill{% if client_notifications_checked %} is-enabled{% endif %}">
|
|
{% if client_notifications_checked %}{{ t.settings_status_enabled }}{% else %}{{ t.settings_status_disabled }}{% endif %}
|
|
</span>
|
|
</div>
|
|
<p>{{ t.settings_section_notifications_help }}</p>
|
|
</div>
|
|
</header>
|
|
<div class="admin-section-body">
|
|
<label class="admin-toggle" for="clientNotifications">
|
|
<input id="clientNotifications" type="checkbox" name="client_notifications_enabled" value="true"{% if client_notifications_checked %} checked{% endif %}>
|
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
|
<span>
|
|
<strong>{{ t.settings_client_notifications_enabled }}</strong>
|
|
<small>{{ t.settings_client_notifications_help }}</small>
|
|
</span>
|
|
</label>
|
|
|
|
<div class="admin-subsection">
|
|
<h3>Web Push / VAPID</h3>
|
|
<div class="admin-note admin-note-warning">
|
|
<p>{{ t.settings_vapid_warning }}</p>
|
|
<p>{{ t.settings_vapid_generate }} <code>cargo run --bin generate_vapid</code></p>
|
|
</div>
|
|
<div class="admin-grid">
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="vapidPublic">{{ t.settings_vapid_public_key }}</label>
|
|
<div class="control">
|
|
<input id="vapidPublic" class="input is-family-monospace" type="text" name="vapid_public_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "vapid_public_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="vapidPrivate">{{ t.settings_vapid_private_key }}</label>
|
|
<div class="control secret-control">
|
|
<input id="vapidPrivate" class="input is-family-monospace" type="password" name="vapid_private_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
|
</div>
|
|
<p class="field-help">{% if vapid_private_key_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
|
{% if vapid_private_key_configured %}
|
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_vapid_private_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
|
{% endif %}
|
|
</div>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="vapidSubject">{{ t.settings_vapid_subject }}</label>
|
|
<div class="control">
|
|
<input id="vapidSubject" class="input" type="text" name="vapid_subject" placeholder="mailto:admin@example.com" value="{% for s in &settings %}{% if s.key == "vapid_subject" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<details class="admin-details">
|
|
<summary>{{ t.settings_push_subscribers }} <span>{{ push_subscribers.len() }}</span></summary>
|
|
<div class="admin-details-body">
|
|
{% if push_subscribers.is_empty() %}
|
|
<p class="field-help">{{ t.settings_push_no_subscribers }}</p>
|
|
{% else %}
|
|
<div class="table-wrap">
|
|
<table class="table is-fullwidth is-striped is-narrow">
|
|
<thead><tr>
|
|
<th>{{ t.settings_push_client }}</th>
|
|
<th>{{ t.settings_push_devices }}</th>
|
|
<th>{{ t.settings_push_language }}</th>
|
|
<th>{{ t.settings_push_updated }}</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for subscriber in &push_subscribers %}
|
|
<tr>
|
|
<td>{{ subscriber.client_name }}</td>
|
|
<td>{{ subscriber.device_count }}</td>
|
|
<td>{{ subscriber.languages }}</td>
|
|
<td class="is-nowrap">{{ subscriber.last_updated }}</td>
|
|
</tr>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
|
|
<div class="admin-subsection">
|
|
<h3>Telegram</h3>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="telegramToken">{{ t.settings_telegram_bot_token }}</label>
|
|
<div class="control secret-control">
|
|
<input id="telegramToken" class="input is-family-monospace" type="password" name="telegram_bot_token" autocomplete="new-password" placeholder="••••••••••••••••">
|
|
</div>
|
|
<p class="field-help">{% if telegram_bot_token_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
|
{% if telegram_bot_token_configured %}
|
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_telegram_bot_token" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="admin-section">
|
|
<header class="admin-section-head">
|
|
<span class="admin-section-icon" aria-hidden="true">🛡️</span>
|
|
<div>
|
|
<h2>{{ t.settings_section_captcha }}</h2>
|
|
<p>{{ t.settings_section_captcha_help }}</p>
|
|
</div>
|
|
</header>
|
|
<div class="admin-section-body">
|
|
<div class="admin-grid">
|
|
<div class="field admin-field">
|
|
<label class="label" for="turnstileSiteKey">{{ t.settings_turnstile_site_key }}</label>
|
|
<div class="control">
|
|
<input id="turnstileSiteKey" class="input is-family-monospace" type="text" name="turnstile_site_key" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "turnstile_site_key" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="turnstileSecret">{{ t.settings_turnstile_secret_key }}</label>
|
|
<div class="control secret-control">
|
|
<input id="turnstileSecret" class="input is-family-monospace" type="password" name="turnstile_secret_key" autocomplete="new-password" placeholder="••••••••••••••••">
|
|
</div>
|
|
<p class="field-help">{% if turnstile_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
|
{% if turnstile_secret_configured %}
|
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_turnstile_secret_key" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="admin-section">
|
|
<header class="admin-section-head">
|
|
<span class="admin-section-icon" aria-hidden="true">🔐</span>
|
|
<div>
|
|
<h2>{{ t.settings_section_oidc }}</h2>
|
|
<p>{{ t.settings_section_oidc_help }}</p>
|
|
</div>
|
|
</header>
|
|
<div class="admin-section-body">
|
|
<div class="admin-toggle-list">
|
|
<label class="admin-toggle" for="passwordAuth">
|
|
<input id="passwordAuth" type="checkbox" name="auth_password_enabled" value="true"{% if auth_password_checked %} checked{% endif %}>
|
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
|
<span><strong>{{ t.settings_auth_password_enabled }}</strong></span>
|
|
</label>
|
|
<label class="admin-toggle" for="ssoAuth">
|
|
<input id="ssoAuth" type="checkbox" name="auth_sso_enabled" value="true"{% if auth_sso_checked %} checked{% endif %}>
|
|
<span class="admin-toggle-track" aria-hidden="true"><span></span></span>
|
|
<span><strong>{{ t.settings_auth_sso_enabled }}</strong></span>
|
|
</label>
|
|
</div>
|
|
|
|
<div class="admin-grid">
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="oidcIssuer">{{ t.settings_oidc_issuer_url }}</label>
|
|
<div class="control">
|
|
<input id="oidcIssuer" class="input" type="url" name="oidc_issuer_url" placeholder="https://keycloak.example.com/realms/myrealm" value="{% for s in &settings %}{% if s.key == "oidc_issuer_url" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="oidcClientId">{{ t.settings_oidc_client_id }}</label>
|
|
<div class="control">
|
|
<input id="oidcClientId" class="input is-family-monospace" type="text" name="oidc_client_id" autocomplete="off" spellcheck="false" value="{% for s in &settings %}{% if s.key == "oidc_client_id" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
<div class="field admin-field">
|
|
<label class="label" for="oidcClientSecret">{{ t.settings_oidc_client_secret }}</label>
|
|
<div class="control secret-control">
|
|
<input id="oidcClientSecret" class="input is-family-monospace" type="password" name="oidc_client_secret" autocomplete="new-password" placeholder="••••••••••••••••">
|
|
</div>
|
|
<p class="field-help">{% if oidc_client_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
|
|
{% if oidc_client_secret_configured %}
|
|
<label class="admin-check admin-check-danger"><input type="checkbox" name="clear_oidc_client_secret" value="true"><span class="admin-check-box">✓</span><span>{{ t.settings_secret_clear }}</span></label>
|
|
{% endif %}
|
|
</div>
|
|
<div class="field admin-field admin-field-wide">
|
|
<label class="label" for="oidcGroups">{{ t.settings_oidc_allowed_groups }}</label>
|
|
<div class="control">
|
|
<input id="oidcGroups" class="input" type="text" name="oidc_allowed_groups" placeholder="admins, web-petting" value="{% for s in &settings %}{% if s.key == "oidc_allowed_groups" %}{{ s.value }}{% endif %}{% endfor %}">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div class="sticky-actions">
|
|
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
|
|
</div>
|
|
</form>
|
|
|
|
<script>
|
|
(function() {
|
|
var colors = [
|
|
'rgba(124,108,255,.18)', 'rgba(255,82,135,.15)',
|
|
'rgba(255,140,38,.18)', 'rgba(0,180,150,.15)',
|
|
'rgba(77,166,255,.18)', 'rgba(255,179,64,.18)',
|
|
'rgba(176,108,255,.16)', 'rgba(34,180,130,.16)'
|
|
];
|
|
var input = document.getElementById('seoKeywordsInput');
|
|
var preview = document.getElementById('seoPreview');
|
|
|
|
function renderSeoPreview() {
|
|
var words = input.value.split(',').map(function(value) { return value.trim(); }).filter(Boolean);
|
|
preview.replaceChildren();
|
|
preview.hidden = words.length === 0;
|
|
words.forEach(function(word, index) {
|
|
var tag = document.createElement('span');
|
|
tag.textContent = word;
|
|
tag.style.background = colors[index % colors.length];
|
|
tag.style.borderRadius = '4px';
|
|
tag.style.padding = '2px 6px';
|
|
tag.style.margin = '2px';
|
|
preview.appendChild(tag);
|
|
});
|
|
}
|
|
|
|
input.addEventListener('input', renderSeoPreview);
|
|
renderSeoPreview();
|
|
})();
|
|
</script>
|
|
{% endblock %}
|