AWG: Added migration
Build and Publish / Build and Publish Docker Image (push) Successful in 3m11s

This commit is contained in:
Ultradesu
2026-07-01 13:53:03 +03:00
parent 77cde17ef9
commit 9ccab69836
8 changed files with 523 additions and 10 deletions
+273
View File
@@ -3,9 +3,11 @@
{% block title %}{{ t.configs_heading }} | {{ t.site_name }}{% endblock title %}
{% block head_extra %}
<script src="https://telegram.org/js/telegram-web-app.js"></script>
<script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
<style>
* { box-sizing: border-box; }
[x-cloak] { display: none !important; }
body { margin: 0; min-height: 100vh; background: #f6f7f8; color: #1d252d; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
a { color: inherit; }
.shell { min-height: 100vh; display: grid; grid-template-columns: 220px minmax(0, 1fr); }
@@ -17,6 +19,13 @@
.topbar { min-height: 48px; display: flex; align-items: center; justify-content: flex-end; gap: 1rem; flex-wrap: wrap; padding: .5rem 1.25rem; border-bottom: 1px solid #dde2e6; background: #fff; }
.user-info { font-size: .875rem; color: #53606d; }
.app-version { font-size: .75rem; color: #9aa4ae; white-space: nowrap; }
.telegram-status-button { min-width: 36px; width: 36px; height: 36px; min-height: 36px; padding: 0; display: inline-grid; place-items: center; position: relative; border-color: #cbd3db; background: #fff; color: #17202a; }
.telegram-status-button .telegram-mark { font-size: .7rem; font-weight: 900; letter-spacing: 0; }
.telegram-dot { position: absolute; right: 4px; top: 4px; width: .55rem; height: .55rem; border-radius: 999px; border: 2px solid #fff; background: #7b8793; }
.telegram-status-button.tone-connected .telegram-dot { background: #2f8f4e; }
.telegram-status-button.tone-pending .telegram-dot { background: #d39a00; }
.telegram-status-button.tone-empty .telegram-dot { background: #69777f; }
.telegram-status-button.tone-disabled .telegram-dot { background: #9d2323; }
.logout-link, .lang-switch a { font-size: .875rem; text-decoration: none; color: #53606d; padding: .25rem .45rem; border-radius: 4px; }
.logout-link:hover, .lang-switch a:hover { background: #eef1f4; color: #1d252d; }
.lang-switch a.active { color: #1d252d; font-weight: 700; }
@@ -104,6 +113,13 @@
.qr-box { display: grid; place-items: center; padding: .75rem; border: 1px solid #dde2e6; border-radius: 6px; background: #fff; }
.qr-box svg { width: min(280px, 100%); height: auto; display: block; }
.modal-actions { display: flex; gap: .5rem; justify-content: flex-end; margin-top: .75rem; flex-wrap: wrap; }
.telegram-modal { width: min(480px, 100%); display: grid; gap: .8rem; }
.telegram-copy { color: #53606d; line-height: 1.45; margin: 0; }
.guide-list { margin: 0; padding-left: 1.2rem; color: #34414f; display: grid; gap: .45rem; line-height: 1.4; }
.guide-list a { color: #17202a; font-weight: 800; }
.guide-list a.disabled { color: #6a7682; pointer-events: none; text-decoration: none; }
.secret-box { display: grid; gap: .35rem; border: 1px solid #dde2e6; border-radius: 6px; background: #f8fafb; padding: .7rem; }
.secret-box code { display: block; padding: .55rem .65rem; font-size: .92rem; white-space: normal; overflow-wrap: anywhere; }
@media (max-width: 760px) {
.shell { grid-template-columns: 1fr; }
.sidebar { display: flex; align-items: center; gap: .75rem; overflow-x: auto; }
@@ -138,6 +154,10 @@
<div class="topbar">
<span class="app-version">v{{ app_version }}</span>
<span class="user-info">{{ user_name }} ({{ user_role }})</span>
<button type="button" x-cloak x-show="telegram.enabled" class="telegram-status-button" :class="`tone-${telegramStatusTone()}`" @click="openTelegramManage()" :title="telegramStatusTitle()" :aria-label="telegramStatusTitle()">
<span class="telegram-mark">TG</span>
<span class="telegram-dot"></span>
</button>
<div class="lang-switch">
<a href="#"{% if t.lang.code() == "en" %} class="active"{% endif %} onclick="location.href='/set-lang?lang=en&next='+encodeURIComponent(location.pathname);return false">EN</a>
<a href="#"{% if t.lang.code() == "ru" %} class="active"{% endif %} onclick="location.href='/set-lang?lang=ru&next='+encodeURIComponent(location.pathname);return false">RU</a>
@@ -341,6 +361,90 @@
</div>
</div>
</template>
<template x-if="telegramModal">
<div class="modal-backdrop" @click.self="telegramModal = null">
<div class="modal telegram-modal">
<div class="modal-head">
<div class="modal-title">
<h3>{{ t.telegram_link_title }}</h3>
<div class="modal-subtitle" x-text="telegramBotLabel()"></div>
</div>
<button class="secondary" @click="telegramModal = null">{{ t.admin_close }}</button>
</div>
<template x-if="telegramModal === 'manage'">
<div>
<p class="telegram-copy" x-text="telegramManageMessage()"></p>
<template x-if="telegram.linked">
<div class="detail-row">
<span class="detail-label">{{ t.telegram_status_connected }}</span>
<span class="detail-value"><code x-text="telegram.telegram_id || ''"></code></span>
</div>
</template>
<template x-if="telegram.pending">
<div class="secret-box">
<span>{{ t.telegram_secret_label }}</span>
<code x-text="telegram.pending_secret || ''"></code>
</div>
</template>
<div class="modal-actions">
<template x-if="telegram.pending">
<button type="button" class="secondary" @click="copyTelegramSecret()" :disabled="telegramBusy || !telegram.pending_secret">{{ t.telegram_copy_secret }}</button>
</template>
<template x-if="telegram.pending">
<button type="button" class="secondary" @click="refreshTelegramStatus()" :disabled="telegramBusy">{{ t.telegram_refresh_status }}</button>
</template>
<button type="button" class="secondary" @click="startTelegramBotLink()" :disabled="telegramBusy" x-text="telegram.linked || telegram.pending ? '{{ t.telegram_change }}' : '{{ t.telegram_connect }}'"></button>
<template x-if="telegram.linked || telegram.pending || telegram.declined">
<button type="button" class="danger" @click="deleteTelegramLink()" :disabled="telegramBusy">{{ t.telegram_delete }}</button>
</template>
</div>
</div>
</template>
<template x-if="telegramModal === 'webapp'">
<div>
<p class="telegram-copy">{{ t.telegram_webapp_message }}</p>
<div class="modal-actions">
<button type="button" class="secondary" @click="declineTelegramLink()" :disabled="telegramBusy">{{ t.telegram_skip }}</button>
<button type="button" @click="linkTelegramWebApp()" :disabled="telegramBusy">{{ t.telegram_save }}</button>
</div>
</div>
</template>
<template x-if="telegramModal === 'manualPrompt'">
<div>
<p class="telegram-copy">{{ t.telegram_manual_message }}</p>
<div class="modal-actions">
<button type="button" class="secondary" @click="declineTelegramLink()" :disabled="telegramBusy">{{ t.telegram_skip }}</button>
<button type="button" @click="startTelegramBotLink()" :disabled="telegramBusy">{{ t.telegram_connect }}</button>
</div>
</div>
</template>
<template x-if="telegramModal === 'manualGuide'">
<div>
<p class="telegram-copy">{{ t.telegram_guide_title }}</p>
<ol class="guide-list">
<li>{{ t.telegram_guide_start }} <a :href="telegramBotUrl()" target="_blank" rel="noreferrer" :class="{ disabled: !telegram.bot_url }">{{ t.telegram_open_bot }}</a></li>
<li>{{ t.telegram_guide_get_id }}</li>
<li>{{ t.telegram_guide_paste }}</li>
</ol>
<div class="secret-box">
<span>{{ t.telegram_secret_label }}</span>
<code x-text="telegram.pending_secret || ''"></code>
</div>
<div class="modal-actions">
<button type="button" class="secondary" @click="declineTelegramLink()" :disabled="telegramBusy">{{ t.telegram_skip }}</button>
<button type="button" class="secondary" @click="copyTelegramSecret()" :disabled="telegramBusy || !telegram.pending_secret">{{ t.telegram_copy_secret }}</button>
<button type="button" @click="refreshTelegramStatus()" :disabled="telegramBusy">{{ t.telegram_refresh_status }}</button>
</div>
</div>
</template>
</div>
</div>
</template>
</div>
<script>
@@ -360,9 +464,27 @@ function configsPage() {
serverConfigs: {},
serverModal: null,
qrModal: null,
telegram: {
enabled: false,
bot_username: '',
bot_url: '',
linked: false,
declined: false,
pending: false,
pending_secret: null,
pending_expires_at: null,
telegram_id: null,
isWebApp: false,
initData: '',
},
telegramModal: null,
telegramBusy: false,
telegramPromptChecked: false,
isAdmin: {% if is_admin %}true{% else %}false{% endif %},
init() {
this.initTelegramWebApp();
this.load();
this.loadTelegramStatus();
if (this.isAdmin) {
this.loadRolloutStatus();
this.rolloutTimer = setInterval(() => this.loadRolloutStatus(), 10000);
@@ -384,6 +506,157 @@ function configsPage() {
}
return data;
},
initTelegramWebApp() {
const webApp = window.Telegram && window.Telegram.WebApp;
if (!webApp) return;
try {
webApp.ready();
webApp.expand();
} catch (_) {}
this.telegram.isWebApp = Boolean(webApp.initData);
this.telegram.initData = webApp.initData || '';
},
async loadTelegramStatus() {
try {
const status = await this.request('/api/telegram-link/status');
this.applyTelegramStatus(status);
this.maybePromptTelegram();
} catch (e) {
console.warn('telegram status failed', e);
}
},
applyTelegramStatus(status) {
this.telegram = {
...this.telegram,
enabled: Boolean(status.enabled),
bot_username: status.bot_username || '',
bot_url: status.bot_url || '',
linked: Boolean(status.linked),
declined: Boolean(status.declined),
pending: Boolean(status.pending),
pending_secret: status.pending_secret || null,
pending_expires_at: status.pending_expires_at || null,
telegram_id: status.telegram_id || null,
};
},
maybePromptTelegram() {
if (this.telegramPromptChecked) return;
this.telegramPromptChecked = true;
if (!this.telegram.enabled || this.telegram.linked || this.telegram.pending || this.telegram.declined) return;
this.telegramModal = (this.telegram.isWebApp && this.telegram.initData) ? 'webapp' : 'manualPrompt';
},
openTelegramManage() {
if (!this.telegram.enabled) return;
if (this.telegram.linked || this.telegram.pending || this.telegram.declined) {
this.telegramModal = 'manage';
return;
}
this.telegramModal = (this.telegram.isWebApp && this.telegram.initData) ? 'webapp' : 'manualPrompt';
},
async linkTelegramWebApp() {
this.telegramBusy = true;
this.clearNotice();
try {
const data = await this.request('/api/telegram-link/webapp', {
method: 'POST',
body: JSON.stringify({ init_data: this.telegram.initData }),
});
this.applyTelegramStatus(data.status || {});
this.telegramModal = null;
this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_saved }}');
} catch (e) {
this.showError(e);
} finally {
this.telegramBusy = false;
}
},
async startTelegramBotLink() {
this.telegramBusy = true;
this.clearNotice();
try {
const data = await this.request('/api/telegram-link/start', { method: 'POST' });
this.applyTelegramStatus(data.status || {});
this.telegramModal = 'manualGuide';
} catch (e) {
this.showError(e);
} finally {
this.telegramBusy = false;
}
},
async refreshTelegramStatus() {
this.telegramBusy = true;
try {
await this.loadTelegramStatus();
if (this.telegram.linked) {
this.telegramModal = 'manage';
this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_saved }}');
}
} finally {
this.telegramBusy = false;
}
},
async copyTelegramSecret() {
if (!this.telegram.pending_secret) return;
this.clearNotice();
try {
await this.copyText(this.telegram.pending_secret);
this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_secret_copied }}');
} catch (e) {
this.showError(e);
}
},
async declineTelegramLink() {
this.telegramBusy = true;
this.clearNotice();
try {
const data = await this.request('/api/telegram-link/decline', { method: 'POST' });
this.applyTelegramStatus(data.status || {});
this.telegramModal = null;
this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_declined }}');
} catch (e) {
this.showError(e);
} finally {
this.telegramBusy = false;
}
},
async deleteTelegramLink() {
if (!confirm('{{ t.telegram_delete_confirm }}')) return;
this.telegramBusy = true;
this.clearNotice();
try {
const data = await this.request('/api/telegram-link/unlink', { method: 'POST' });
this.applyTelegramStatus(data.status || {});
this.telegramModal = null;
this.setNotice('success', '{{ t.notice_success_title }}', '{{ t.telegram_unlinked }}');
} catch (e) {
this.showError(e);
} finally {
this.telegramBusy = false;
}
},
telegramBotUrl() {
return this.telegram.bot_url || '#';
},
telegramBotLabel() {
return this.telegram.bot_username ? `@${this.telegram.bot_username}` : '{{ t.telegram_bot_unconfigured }}';
},
telegramStatusTone() {
if (!this.telegram.enabled) return 'disabled';
if (this.telegram.pending) return 'pending';
if (this.telegram.linked) return 'connected';
return 'empty';
},
telegramStatusTitle() {
if (!this.telegram.enabled) return '{{ t.telegram_status_disabled }}';
if (this.telegram.pending) return '{{ t.telegram_status_pending }}';
if (this.telegram.linked) return '{{ t.telegram_status_connected }}';
return '{{ t.telegram_status_empty }}';
},
telegramManageMessage() {
if (this.telegram.pending) return '{{ t.telegram_pending_message }}';
if (this.telegram.linked) return '{{ t.telegram_connected_message }}';
return '{{ t.telegram_not_connected_message }}';
},
async load() {
this.error = '';
this.busy = true;