2025-12-29 20:01:55 +03:00
<!DOCTYPE html>
< html lang = "ru" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" >
< title > Play Life Backend - Admin Panel< / title >
< style >
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
h1 {
color: white;
text-align: center;
margin-bottom: 30px;
font-size: 2.5em;
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 20px;
}
.card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
.card h2 {
color: #667eea;
margin-bottom: 20px;
font-size: 1.3em;
display: flex;
align-items: center;
gap: 10px;
}
.card textarea {
width: 100%;
min-height: 150px;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 14px;
font-family: monospace;
resize: vertical;
margin-bottom: 15px;
}
.card textarea:focus {
outline: none;
border-color: #667eea;
}
.card button {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
width: 100%;
}
.card button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.card button:active {
transform: translateY(0);
}
.card button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.result {
margin-top: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 5px;
border-left: 4px solid #667eea;
}
.result h3 {
color: #333;
margin-bottom: 10px;
font-size: 1.1em;
}
.result pre {
background: #2d2d2d;
color: #f8f8f2;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
font-size: 12px;
line-height: 1.5;
max-height: 400px;
overflow-y: auto;
}
.result.success {
border-left-color: #4caf50;
}
.result.error {
border-left-color: #f44336;
}
.result.loading {
border-left-color: #ff9800;
}
.status {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-left: 10px;
}
.status.success {
background: #4caf50;
color: white;
}
.status.error {
background: #f44336;
color: white;
}
.status.loading {
background: #ff9800;
color: white;
}
< / style >
< / head >
< body >
< div class = "container" >
< h1 > 🎯 Play Life Backend - Admin Panel< / h1 >
< div class = "grid" >
<!-- Message Post Card -->
< div class = "card" >
< h2 >
📨 Message Post
< span class = "status" id = "messageStatus" style = "display: none;" > < / span >
< / h2 >
< textarea id = "messageText" placeholder = "Введите сообщение с паттернами * * Project + 10 . 5 * * или * * Project-5 . 0 * * . . .
Пример:
Сегодня работал над проектами:
**Frontend+15.5**
**Backend+8.0**
**Design-2.5**">< / textarea >
< button onclick = "sendMessage()" > Отправить сообщение< / button >
< div id = "messageResult" > < / div >
< / div >
<!-- Daily Report Trigger Card -->
< div class = "card" >
< h2 >
📈 Daily Report Trigger
< span class = "status" id = "dailyReportStatus" style = "display: none;" > < / span >
< / h2 >
< p style = "margin-bottom: 15px; color: #666;" >
2025-12-30 18:27:12 +03:00
Нажмите кнопку для отправки ежедневного отчёта по Score и Целям в Telegram (обычно отправляется автоматически в 23:59).
2025-12-29 20:01:55 +03:00
< / p >
< button onclick = "triggerDailyReport()" > Отправить отчёт< / button >
< div id = "dailyReportResult" > < / div >
< / div >
<!-- Weekly Goals Setup Card -->
< div class = "card" >
< h2 >
🎯 Weekly Goals Setup
< span class = "status" id = "goalsStatus" style = "display: none;" > < / span >
< / h2 >
< p style = "margin-bottom: 15px; color: #666;" >
Нажмите кнопку для установки целей на текущую неделю на основе медианы за последние 3 месяца (с отправкой в чат). Обычно срабатывает автоматически в начале недели.
< / p >
< button onclick = "setupWeeklyGoals()" > Обновить цели< / button >
< div id = "goalsResult" > < / div >
< / div >
< / div >
< / div >
< script >
function getApiUrl() {
// Автоматически определяем URL текущего хоста
// Админка обслуживается тем же бекендом, поэтому используем текущий origin
return window.location.origin;
}
function showStatus(elementId, status, text) {
const statusEl = document.getElementById(elementId);
statusEl.textContent = text;
statusEl.className = `status ${status}`;
statusEl.style.display = 'inline-block';
}
function hideStatus(elementId) {
document.getElementById(elementId).style.display = 'none';
}
function showResult(elementId, data, isError = false, isLoading = false) {
const resultEl = document.getElementById(elementId);
resultEl.innerHTML = '';
if (isLoading) {
resultEl.innerHTML = '< div class = "result loading" > < h3 > ⏳ Загрузка...< / h3 > < / div > ';
return;
}
const div = document.createElement('div');
div.className = `result ${isError ? 'error' : 'success'}`;
const h3 = document.createElement('h3');
h3.textContent = isError ? '❌ Ошибка' : '✅ Успешно';
div.appendChild(h3);
const pre = document.createElement('pre');
pre.textContent = JSON.stringify(data, null, 2);
div.appendChild(pre);
resultEl.appendChild(div);
}
async function sendMessage() {
const text = document.getElementById('messageText').value.trim();
if (!text) {
alert('Пожалуйста, введите сообщение');
return;
}
showStatus('messageStatus', 'loading', 'Отправка...');
showResult('messageResult', null, false, true);
try {
const response = await fetch(`${getApiUrl()}/message/post`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: {
text: text
}
})
});
const data = await response.json();
if (response.ok) {
showStatus('messageStatus', 'success', 'Успешно');
showResult('messageResult', data, false);
} else {
showStatus('messageStatus', 'error', 'Ошибка');
showResult('messageResult', data, true);
}
} catch (error) {
showStatus('messageStatus', 'error', 'Ошибка');
showResult('messageResult', { error: error.message }, true);
}
}
async function setupWeeklyGoals() {
showStatus('goalsStatus', 'loading', 'Обновление...');
showResult('goalsResult', null, false, true);
try {
const response = await fetch(`${getApiUrl()}/weekly_goals/setup`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const data = await response.json();
if (response.ok) {
showStatus('goalsStatus', 'success', 'Успешно');
showResult('goalsResult', data, false);
} else {
showStatus('goalsStatus', 'error', 'Ошибка');
showResult('goalsResult', data, true);
}
} catch (error) {
showStatus('goalsStatus', 'error', 'Ошибка');
showResult('goalsResult', { error: error.message }, true);
}
}
async function triggerDailyReport() {
showStatus('dailyReportStatus', 'loading', 'Отправка...');
showResult('dailyReportResult', null, false, true);
try {
const response = await fetch(`${getApiUrl()}/daily-report/trigger`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
}
});
const data = await response.json();
if (response.ok) {
showStatus('dailyReportStatus', 'success', 'Успешно');
showResult('dailyReportResult', data, false);
} else {
showStatus('dailyReportStatus', 'error', 'Ошибка');
showResult('dailyReportResult', data, true);
}
} catch (error) {
showStatus('dailyReportStatus', 'error', 'Ошибка');
showResult('dailyReportResult', { error: error.message }, true);
}
}
// Разрешаем отправку формы по Enter (Ctrl+Enter для textarea)
document.getElementById('messageText').addEventListener('keydown', function(e) {
if (e.ctrlKey & & e.key === 'Enter') {
sendMessage();
}
});
< / script >
< / body >
< / html >