Files
umbrix/update-server/admin/index.html
Umbrix Developer 76a374950f feat: mobile-like window size and always-visible stats
- Changed window size to mobile phone format (400x800)
- Removed width condition for ActiveProxyFooter - now always visible
- Added run-umbrix.sh launch script with icon copying
- Stats cards now display on all screen sizes
2026-01-17 13:09:20 +03:00

654 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>🚀 Umbrix Update Manager</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: #1a1f2e;
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 800px;
margin: 0 auto;
}
.header {
text-align: center;
color: #ffffff;
margin-bottom: 30px;
}
.header h1 {
font-size: 2.5em;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #b4bcc9;
margin-bottom: 30px;
}
.panel {
background: #252d3d;
border-radius: 16px;
padding: 30px;
box-shadow: 0 10px 40px rgba(0,0,0,0.4);
border: 1px solid #2d3548;
}
.current-version {
background: #1e2533;
padding: 20px;
border-radius: 12px;
margin-bottom: 20px;
border-left: 4px solid #4ade80;
border: 1px solid #2d3548;
}
.current-version h3 {
margin-bottom: 12px;
color: #4ade80;
font-size: 1.1em;
}
.version-info {
display: grid;
grid-template-columns: 150px 1fr;
gap: 10px;
font-size: 0.9em;
}
.version-info strong {
color: #9ca3af;
}
.version-info span {
color: #ffffff;
}
.badge {
display: inline-block;
padding: 5px 12px;
border-radius: 8px;
font-size: 0.8em;
font-weight: 600;
}
.badge.beta {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border: 1px solid rgba(239, 68, 68, 0.3);
}
.badge.stable {
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.3);
}
.form-group {
margin-bottom: 20px;
}
.form-group h3 {
color: #ffffff;
margin-bottom: 15px;
font-size: 1.1em;
}
label {
display: block;
font-weight: 600;
margin-bottom: 8px;
color: #ffffff;
font-size: 0.95em;
}
.hint {
font-size: 0.85em;
color: #9ca3af;
margin-top: 4px;
}
input[type="text"],
input[type="url"],
input[type="datetime-local"],
textarea {
width: 100%;
padding: 12px;
border: 2px solid #2d3548;
border-radius: 12px;
font-size: 1em;
transition: all 0.3s;
background: #1e2533;
color: #ffffff;
}
input::placeholder,
textarea::placeholder {
color: #6b7280;
}
input:focus,
textarea:focus {
outline: none;
border-color: #4ade80;
background: #252d3d;
}
textarea {
min-height: 120px;
resize: vertical;
font-family: inherit;
}
.checkbox-group {
display: flex;
align-items: center;
gap: 10px;
}
input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #4ade80;
}
.checkbox-group label {
color: #e5e7eb;
font-weight: 400;
margin-bottom: 0;
}
.button-group {
display: flex;
gap: 15px;
margin-top: 30px;
}
button {
flex: 1;
padding: 15px;
border: none;
border-radius: 12px;
font-size: 1em;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary {
background: #4ade80;
color: #1a1f2e;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(74, 222, 128, 0.3);
background: #5ef590;
}
.btn-secondary {
background: #2d3548;
color: #ffffff;
border: 1px solid #3d4658;
}
.btn-secondary:hover {
background: #363f54;
border-color: #4ade80;
}
.status {
margin-top: 20px;
padding: 15px;
border-radius: 12px;
display: none;
border: 1px solid;
}
.status.success {
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border-color: rgba(74, 222, 128, 0.3);
}
.status.error {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
border-color: rgba(239, 68, 68, 0.3);
}
.tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
border-bottom: 2px solid #2d3548;
}
.tab {
padding: 12px 24px;
background: transparent;
border: none;
color: #9ca3af;
cursor: pointer;
font-size: 1em;
font-weight: 600;
border-bottom: 3px solid transparent;
transition: all 0.3s;
}
.tab:hover {
color: #ffffff;
}
.tab.active {
color: #4ade80;
border-bottom-color: #4ade80;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
.history-list {
margin-top: 20px;
}
.history-item {
background: #1e2533;
padding: 15px;
border-radius: 12px;
margin-bottom: 10px;
border: 1px solid #2d3548;
display: flex;
justify-content: space-between;
align-items: center;
}
.history-item.current {
border-left: 4px solid #4ade80;
}
.history-info {
flex: 1;
}
.history-version {
color: #ffffff;
font-weight: 600;
font-size: 1.1em;
margin-bottom: 5px;
}
.history-meta {
color: #9ca3af;
font-size: 0.85em;
}
.btn-restore {
background: #f59e0b;
color: #1a1f2e;
padding: 8px 16px;
border: none;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
transition: all 0.3s;
}
.btn-restore:hover {
background: #fbbf24;
transform: translateY(-2px);
}
.btn-restore:disabled {
background: #374151;
color: #6b7280;
cursor: not-allowed;
transform: none;
}
@media (max-width: 768px) {
.button-group {
flex-direction: column-reverse;
}
.history-item {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🚀 Umbrix Update Manager</h1>
<p class="subtitle">Панель управления обновлениями приложения</p>
</div>
<div class="panel">
<div class="tabs">
<button class="tab active" onclick="switchTab('update')">📝 Обновление</button>
<button class="tab" onclick="switchTab('history')">🕐 История версий</button>
</div>
<div id="updateTab" class="tab-content active">
<div class="current-version">
<h3>📦 Текущая версия</h3>
<div class="version-info">
<strong>Версия:</strong>
<span id="currentVersion">Ошибка загрузки</span>
<strong>Build:</strong>
<span id="currentBuild">-</span>
<strong>Статус:</strong>
<span id="currentStatus">-</span>
<strong>Опубликовано:</strong>
<span id="currentPublished">-</span>
</div>
</div>
<form id="updateForm">
<div class="form-group">
<label for="version">Версия *</label>
<input
type="text"
id="version"
name="version"
placeholder="2.5.8"
required
>
<div class="hint">Формат: X.Y.Z (например: 2.5.8)</div>
</div>
<div class="form-group">
<label for="buildNumber">Build Number *</label>
<input
type="text"
id="buildNumber"
name="buildNumber"
placeholder="258"
required
>
<div class="hint">Число, соответствующее версии</div>
</div>
<div class="form-group">
<label for="downloadUrl">URL для скачивания APK *</label>
<input
type="url"
id="downloadUrl"
name="downloadUrl"
placeholder="https://api.umbrix.net/downloads/umbrix-2.5.8.apk"
required
>
<div class="hint">Полный URL к APK файлу на вашем сервере</div>
</div>
<div class="form-group">
<label for="releaseNotes">Описание обновления</label>
<textarea
id="releaseNotes"
name="releaseNotes"
placeholder="🎉 Что нового в этой версии:
✨ Новые функции:
- Улучшена стабильность
- Исправлены ошибки
Что нового в этой версии? (поддерживаются эмодзи)"
></textarea>
<div class="hint">Что нового в этой версии? (поддерживаются эмодзи)</div>
</div>
<div class="form-group">
<label for="publishedAt">Дата публикации</label>
<input
type="datetime-local"
id="publishedAt"
name="publishedAt"
>
<div class="hint">Оставьте пустым для текущей даты</div>
</div>
<div class="form-group">
<label for="minVersion">Минимальная требуемая версия</label>
<input
type="text"
id="minVersion"
name="minVersion"
placeholder="2.0.0"
>
<div class="hint">Версии ниже не смогут обновиться (опционально)</div>
</div>
<div class="checkbox-group">
<input
type="checkbox"
id="isPrerelease"
name="isPrerelease"
>
<label for="isPrerelease">Это бета-версия (предварительный релиз)</label>
</div>
<div class="button-group">
<button type="button" class="btn-secondary" onclick="loadCurrentVersion()">
📂 Загрузить текущую
</button>
<button type="submit" class="btn-primary">
💾 Сохранить обновление
</button>
</div>
<div class="status" id="status"></div>
</form>
</div>
<div id="historyTab" class="tab-content">
<div class="current-version">
<h3>📚 История всех версий</h3>
<p style="color: #9ca3af; margin-top: 10px;">Все сохранённые версии с возможностью отката</p>
</div>
<div class="history-list" id="historyList">
<p style="text-align: center; color: #9ca3af; padding: 20px;">Загрузка...</p>
</div>
</div>
</div>
</div>
<script>
// Load current version on page load
window.addEventListener('DOMContentLoaded', () => {
loadCurrentVersion();
loadHistory();
});
function switchTab(tab) {
// Update tab buttons
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
// Update tab content
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
document.getElementById(tab + 'Tab').classList.add('active');
if (tab === 'history') {
loadHistory();
}
}
async function loadHistory() {
const historyList = document.getElementById('historyList');
try {
const response = await fetch('history.php');
if (!response.ok) throw new Error('Не удалось загрузить историю');
const data = await response.json();
if (!data.success || data.versions.length === 0) {
historyList.innerHTML = '<p style="text-align: center; color: #9ca3af; padding: 20px;">История пуста</p>';
return;
}
historyList.innerHTML = data.versions.map(v => `
<div class="history-item ${v.is_current ? 'current' : ''}">
<div class="history-info">
<div class="history-version">
${v.is_current ? '🟢 ' : ''}v${v.version} (Build ${v.build_number})
${v.is_prerelease ? '<span class="badge beta">Бета</span>' : '<span class="badge stable">Стабильная</span>'}
</div>
<div class="history-meta">
📅 ${new Date(v.timestamp * 1000).toLocaleString('ru-RU')}
${v.backup_date ? ` • Бэкап: ${v.backup_date.replace('_', ' ').replace(/-/g, '.')}` : ''}
• 💾 ${(v.size / 1024).toFixed(1)} KB
</div>
${v.release_notes ? `<div style="color: #6b7280; font-size: 0.85em; margin-top: 8px; max-width: 500px; white-space: pre-wrap;">${v.release_notes.substring(0, 100)}${v.release_notes.length > 100 ? '...' : ''}</div>` : ''}
</div>
<button
class="btn-restore"
onclick="restoreVersion('${v.filename}')"
${v.is_current ? 'disabled' : ''}
>
${v.is_current ? '✓ Текущая' : '↩️ Откатить'}
</button>
</div>
`).join('');
} catch (error) {
console.error('Error loading history:', error);
historyList.innerHTML = '<p style="text-align: center; color: #ef4444; padding: 20px;">❌ Ошибка загрузки истории</p>';
}
}
async function restoreVersion(filename) {
if (!confirm(`Откатить на версию из ${filename}?\n\nТекущая версия будет сохранена в бэкап.`)) {
return;
}
try {
const response = await fetch('restore.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ filename })
});
const result = await response.json();
if (result.success) {
alert(`${result.message}\n\nВосстановлена версия: v${result.version} (Build ${result.build_number})`);
await loadCurrentVersion();
await loadHistory();
switchTab('update');
} else {
alert('❌ ' + result.message);
}
} catch (error) {
alert('❌ Ошибка отката: ' + error.message);
}
}
async function loadCurrentVersion() {
try {
const response = await fetch('../latest.json');
if (!response.ok) {
throw new Error('Не удалось загрузить текущую версию');
}
const data = await response.json();
// Update current version display
document.getElementById('currentVersion').textContent = data.version || '-';
document.getElementById('currentBuild').textContent = data.build_number || '-';
document.getElementById('currentStatus').innerHTML = data.is_prerelease
? '<span class="badge beta">Бета-версия</span>'
: '<span class="badge stable">Стабильная</span>';
document.getElementById('currentPublished').textContent = data.published_at
? new Date(data.published_at).toLocaleString('ru-RU')
: '-';
} catch (error) {
console.error('Error loading current version:', error);
document.getElementById('currentVersion').textContent = 'Ошибка загрузки';
}
}
document.getElementById('updateForm').addEventListener('submit', async function(e) {
e.preventDefault();
const statusDiv = document.getElementById('status');
statusDiv.style.display = 'none';
const formData = new FormData(e.target);
const data = {
version: formData.get('version'),
build_number: formData.get('buildNumber'),
download_url: formData.get('downloadUrl'),
release_notes: formData.get('releaseNotes'),
published_at: formData.get('publishedAt') || new Date().toISOString(),
min_required_version: formData.get('minVersion') || null,
is_prerelease: formData.get('isPrerelease') === 'on'
};
try {
const response = await fetch('save.php', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(data)
});
const result = await response.json();
if (result.success) {
showStatus('success', '✅ ' + result.message);
await loadCurrentVersion();
e.target.reset();
} else {
showStatus('error', '❌ ' + result.message);
}
} catch (error) {
showStatus('error', '❌ Ошибка сохранения: ' + error.message);
}
});
function showStatus(type, message) {
const statusDiv = document.getElementById('status');
statusDiv.className = 'status ' + type;
statusDiv.textContent = message;
statusDiv.style.display = 'block';
// Auto-hide after 5 seconds
setTimeout(() => {
statusDiv.style.display = 'none';
}, 5000);
}
</script>
</body>
</html>