Files
web-petting/templates/admin/settings.html
T
Aleksandr Bogomiakov a9188f919b
Build and Publish / Build and Publish Docker Image (push) Successful in 1m45s
Added Cloudflare R2 media storage support
2026-08-09 01:32:10 +01:00

413 lines
26 KiB
HTML

{% extends "admin/layout.html" %}
{% let active_page = "settings" %}
{% block title %}{{ t.settings_title }}{% endblock %}
{% block content %}
<div class="page-head settings-page-head">
<div>
<h1>{{ t.settings_title }}</h1>
<p>{{ t.settings_intro }}</p>
</div>
</div>
{% if saved %}
<div class="notification is-success is-light settings-message">{{ t.settings_saved }}</div>
{% endif %}
{% if let Some(message) = error %}
<div class="notification is-danger is-light settings-message">{{ message }}</div>
{% endif %}
<form id="settingsForm" class="settings-form" method="post" action="/admin/settings/save">
<section class="settings-section">
<header class="settings-section-head">
<span class="settings-section-icon" aria-hidden="true">🌐</span>
<div>
<h2>{{ t.settings_section_general }}</h2>
<p>{{ t.settings_section_general_help }}</p>
</div>
</header>
<div class="settings-section-body">
<div class="settings-grid">
<div class="field settings-field settings-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 settings-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 settings-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 settings-field settings-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 settings-field settings-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="settings-section">
<header class="settings-section-head">
<span class="settings-section-icon" aria-hidden="true">☁️</span>
<div>
<div class="settings-title-row">
<h2>{{ t.settings_section_storage }}</h2>
<span class="settings-status{% 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="settings-section-body">
<label class="settings-toggle" for="r2Enabled">
<input id="r2Enabled" type="checkbox" name="r2_enabled" value="true"{% if r2_enabled_checked %} checked{% endif %}>
<span class="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="settings-note settings-note-info">{{ t.settings_r2_migration_help }}</div>
<div class="settings-grid">
<div class="field settings-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 settings-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 settings-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 settings-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="settings-help">{% if r2_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
{% if r2_secret_configured %}
<label class="secret-clear"><input type="checkbox" name="clear_r2_secret_access_key" value="true"> {{ t.settings_secret_clear }}</label>
{% endif %}
</div>
</div>
</div>
</section>
<section class="settings-section">
<header class="settings-section-head">
<span class="settings-section-icon" aria-hidden="true">🔔</span>
<div>
<div class="settings-title-row">
<h2>{{ t.settings_section_notifications }}</h2>
<span class="settings-status{% 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="settings-section-body">
<label class="settings-toggle" for="clientNotifications">
<input id="clientNotifications" type="checkbox" name="client_notifications_enabled" value="true"{% if client_notifications_checked %} checked{% endif %}>
<span class="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="settings-subsection">
<h3>Web Push / VAPID</h3>
<div class="settings-note settings-note-warning">
<p>{{ t.settings_vapid_warning }}</p>
<p>{{ t.settings_vapid_generate }} <code>cargo run --bin generate_vapid</code></p>
</div>
<div class="settings-grid">
<div class="field settings-field settings-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 settings-field settings-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="settings-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="secret-clear"><input type="checkbox" name="clear_vapid_private_key" value="true"> {{ t.settings_secret_clear }}</label>
{% endif %}
</div>
<div class="field settings-field settings-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="settings-details">
<summary>{{ t.settings_push_subscribers }} <span>{{ push_subscribers.len() }}</span></summary>
<div class="settings-details-body">
{% if push_subscribers.is_empty() %}
<p class="settings-help">{{ t.settings_push_no_subscribers }}</p>
{% else %}
<div class="settings-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="settings-subsection">
<h3>Telegram</h3>
<div class="field settings-field settings-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="settings-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="secret-clear"><input type="checkbox" name="clear_telegram_bot_token" value="true"> {{ t.settings_secret_clear }}</label>
{% endif %}
</div>
</div>
</div>
</section>
<section class="settings-section">
<header class="settings-section-head">
<span class="settings-section-icon" aria-hidden="true">🛡️</span>
<div>
<h2>{{ t.settings_section_captcha }}</h2>
<p>{{ t.settings_section_captcha_help }}</p>
</div>
</header>
<div class="settings-section-body">
<div class="settings-grid">
<div class="field settings-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 settings-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="settings-help">{% if turnstile_secret_configured %}{{ t.settings_secret_saved }}{% else %}{{ t.settings_secret_not_set }}{% endif %}</p>
{% if turnstile_secret_configured %}
<label class="secret-clear"><input type="checkbox" name="clear_turnstile_secret_key" value="true"> {{ t.settings_secret_clear }}</label>
{% endif %}
</div>
</div>
</div>
</section>
<section class="settings-section">
<header class="settings-section-head">
<span class="settings-section-icon" aria-hidden="true">🔐</span>
<div>
<h2>{{ t.settings_section_oidc }}</h2>
<p>{{ t.settings_section_oidc_help }}</p>
</div>
</header>
<div class="settings-section-body">
<div class="settings-toggle-list">
<label class="settings-toggle" for="passwordAuth">
<input id="passwordAuth" type="checkbox" name="auth_password_enabled" value="true"{% if auth_password_checked %} checked{% endif %}>
<span class="toggle-track" aria-hidden="true"><span></span></span>
<span><strong>{{ t.settings_auth_password_enabled }}</strong></span>
</label>
<label class="settings-toggle" for="ssoAuth">
<input id="ssoAuth" type="checkbox" name="auth_sso_enabled" value="true"{% if auth_sso_checked %} checked{% endif %}>
<span class="toggle-track" aria-hidden="true"><span></span></span>
<span><strong>{{ t.settings_auth_sso_enabled }}</strong></span>
</label>
</div>
<div class="settings-grid">
<div class="field settings-field settings-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 settings-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 settings-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="settings-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="secret-clear"><input type="checkbox" name="clear_oidc_client_secret" value="true"> {{ t.settings_secret_clear }}</label>
{% endif %}
</div>
<div class="field settings-field settings-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="settings-save-bar">
<button type="submit" class="button is-primary">{{ t.settings_save }}</button>
</div>
</form>
<style>
.settings-page-head { align-items:flex-end; margin-bottom:1.25rem; }
.settings-page-head h1 { font-size:1.55rem; }
.settings-page-head p { margin-top:.25rem; color:#777; font-size:.9rem; }
.settings-message { border-radius:10px; margin-bottom:1rem; }
.settings-form { display:flex; flex-direction:column; gap:1rem; }
.settings-section { background:#fff; border:1px solid #e7e5ef; border-radius:14px; overflow:hidden; box-shadow:0 2px 10px rgba(42,34,80,.035); }
.settings-section-head { display:grid; grid-template-columns:42px minmax(0,1fr); gap:.8rem; padding:1rem 1.15rem; background:linear-gradient(180deg,#fff,#fbfaff); border-bottom:1px solid #eceaf2; }
.settings-section-icon { display:flex; align-items:center; justify-content:center; width:42px; height:42px; border-radius:11px; background:#efedff; font-size:1.2rem; }
.settings-section-head h2 { margin:0; font-size:1.05rem; font-weight:750; color:#302c49; }
.settings-section-head p { margin:.2rem 0 0; max-width:680px; color:#817c94; font-size:.82rem; line-height:1.45; }
.settings-title-row { display:flex; align-items:center; flex-wrap:wrap; gap:.55rem; }
.settings-status { display:inline-flex; align-items:center; padding:.12rem .48rem; border-radius:99px; background:#eeedf2; color:#777184; font-size:.67rem; font-weight:700; }
.settings-status.is-enabled { background:#dcf7e8; color:#177349; }
.settings-section-body { padding:1.15rem; }
.settings-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1rem 1.1rem; }
.settings-field { min-width:0; margin:0 !important; }
.settings-field-wide { grid-column:1 / -1; }
.settings-field .label { margin-bottom:.38rem; font-size:.78rem; font-weight:700; color:#514c66 !important; }
.settings-field .input, .settings-field .textarea { border-radius:8px; box-shadow:none; font-size:.88rem; }
.settings-field .input:focus, .settings-field .textarea:focus { border-color:#7c6cff !important; box-shadow:0 0 0 2px rgba(124,108,255,.12); }
.settings-field .textarea { min-height:76px; resize:vertical; }
.settings-help { margin-top:.35rem; color:#8b869b; font-size:.72rem; line-height:1.4; }
.secret-control { position:relative; }
.secret-control::after { content:"🔒"; position:absolute; right:.75rem; top:50%; transform:translateY(-50%); font-size:.78rem; opacity:.48; pointer-events:none; }
.secret-control .input { padding-right:2.25rem; letter-spacing:.05em; }
.secret-clear { display:inline-flex; align-items:center; gap:.35rem; margin-top:.38rem; color:#a04e58 !important; font-size:.7rem; cursor:pointer; }
.secret-clear input { accent-color:#b85c66; }
.settings-toggle-list { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.7rem; margin-bottom:1rem; }
.settings-toggle { display:flex; align-items:flex-start; gap:.7rem; padding:.75rem .85rem; border:1px solid #e8e5f2; border-radius:10px; background:#fbfaff; cursor:pointer; }
.settings-toggle > input { position:absolute; opacity:0; pointer-events:none; }
.toggle-track { flex:0 0 auto; width:38px; height:22px; padding:2px; border-radius:99px; background:#ccc8d6; transition:.18s; }
.toggle-track span { display:block; width:18px; height:18px; border-radius:50%; background:#fff; box-shadow:0 1px 4px rgba(0,0,0,.2); transition:.18s; }
.settings-toggle > input:checked + .toggle-track { background:#7567e8; }
.settings-toggle > input:checked + .toggle-track span { transform:translateX(16px); }
.settings-toggle > input:focus-visible + .toggle-track { outline:2px solid #7c6cff; outline-offset:2px; }
.settings-toggle strong { display:block; color:#3e3956; font-size:.82rem; line-height:1.35; }
.settings-toggle small { display:block; margin-top:.15rem; color:#8a849c; font-size:.7rem; line-height:1.35; }
.settings-note { margin:.9rem 0 1rem; padding:.75rem .9rem; border:1px solid; border-radius:9px; font-size:.76rem; line-height:1.5; }
.settings-note p + p { margin-top:.35rem; }
.settings-note-info { background:#f1f7ff; border-color:#d7e8fa; color:#42627f; }
.settings-note-warning { background:#fff8e8; border-color:#f1dfb4; color:#785f28; }
.settings-note code { display:inline-block; margin:.2rem 0 0; padding:.12rem .35rem; border-radius:4px; background:rgba(255,255,255,.7); color:inherit; user-select:all; }
.settings-subsection { margin-top:1.1rem; padding-top:1.1rem; border-top:1px solid #eeecf3; }
.settings-subsection h3 { margin:0 0 .7rem; color:#514b69; font-size:.82rem; font-weight:800; text-transform:uppercase; letter-spacing:.04em; }
.settings-details { margin-top:1rem; border:1px solid #e8e5f2; border-radius:9px; background:#fcfbff; overflow:hidden; }
.settings-details summary { display:flex; justify-content:space-between; align-items:center; padding:.7rem .85rem; cursor:pointer; color:#514b69; font-size:.78rem; font-weight:700; }
.settings-details summary span { min-width:24px; padding:.05rem .4rem; border-radius:99px; background:#ece9ff; color:#6257bb; text-align:center; }
.settings-details-body { padding:0 .85rem .85rem; }
.settings-table-wrap { overflow-x:auto; }
.settings-table-wrap .table { margin:0; background:transparent; font-size:.75rem; }
.is-nowrap { white-space:nowrap; }
.seo-preview { margin-top:.5rem; padding:.5rem .65rem; border:1px solid #ebe9ef; border-radius:7px; background:#fafafa; min-height:2rem; line-height:2; font-size:.78rem; }
.settings-save-bar { position:sticky; bottom:.75rem; z-index:10; display:flex; justify-content:flex-end; padding:.65rem; border:1px solid rgba(222,218,238,.9); border-radius:12px; background:rgba(255,255,255,.92); box-shadow:0 8px 28px rgba(46,38,86,.12); backdrop-filter:blur(8px); }
.settings-save-bar .button { min-width:150px; border-radius:8px; font-weight:700; }
@media (max-width:700px) {
.settings-grid, .settings-toggle-list { grid-template-columns:1fr; }
.settings-field-wide { grid-column:auto; }
.settings-section-head { grid-template-columns:36px minmax(0,1fr); padding:.9rem; }
.settings-section-icon { width:36px; height:36px; }
.settings-section-body { padding:.9rem; }
.settings-save-bar { bottom:4.15rem; }
.settings-save-bar .button { width:100%; }
}
</style>
<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 %}