This commit is contained in:
2026-03-16 18:57:22 +03:00
parent 65eca64a5f
commit 668e62d652
18 changed files with 6386 additions and 1347 deletions

View File

@@ -0,0 +1,962 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Mobile Debug - Rabota.Today</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
}
body {
background: #0b1c34;
padding: 15px;
color: #333;
}
.container {
max-width: 500px;
margin: 0 auto;
}
.card {
background: white;
border-radius: 24px;
padding: 20px;
margin-bottom: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
}
h1 {
font-size: 22px;
margin-bottom: 15px;
color: #0b1c34;
}
h2 {
font-size: 18px;
margin-bottom: 12px;
color: #1f3f60;
}
h3 {
font-size: 16px;
margin: 10px 0;
color: #1f3f60;
}
.info-row {
background: #f5f9ff;
padding: 12px;
border-radius: 12px;
margin-bottom: 8px;
font-size: 14px;
word-break: break-word;
}
.label {
font-weight: 600;
color: #1f3f60;
margin-bottom: 4px;
}
.value {
color: #333;
font-family: monospace;
font-size: 13px;
overflow-x: auto;
white-space: pre-wrap;
word-wrap: break-word;
}
.success { color: #10b981; font-weight: 600; }
.error { color: #ef4444; font-weight: 600; }
.warning { color: #f59e0b; font-weight: 600; }
.info { color: #3b82f6; font-weight: 600; }
button {
background: #0b1c34;
color: white;
border: none;
padding: 14px 16px;
border-radius: 40px;
font-weight: 600;
font-size: 15px;
width: 100%;
margin: 8px 0;
cursor: pointer;
transition: 0.2s;
-webkit-tap-highlight-color: transparent;
}
button:active {
transform: scale(0.98);
background: #1b3f6b;
}
button.secondary {
background: #e5e7eb;
color: #1f3f60;
}
button.secondary:active {
background: #d1d5db;
}
button.small {
padding: 8px 12px;
font-size: 13px;
width: auto;
margin: 5px;
}
.test-result {
margin-top: 15px;
padding: 15px;
background: #f5f5f5;
border-radius: 16px;
font-size: 13px;
max-height: 300px;
overflow-y: auto;
display: none;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid #dee9f5;
}
.loader {
display: inline-block;
width: 20px;
height: 20px;
border: 3px solid #f3f3f3;
border-top: 3px solid #0b1c34;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-right: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.flex {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
flex-wrap: wrap;
}
.flex-start {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 10px;
flex-wrap: wrap;
}
.ip-input {
width: 100%;
padding: 12px 16px;
border: 2px solid #dee9f5;
border-radius: 30px;
font-size: 15px;
margin-bottom: 10px;
}
.protocol-selector {
display: flex;
gap: 10px;
margin: 15px 0;
background: #f0f7ff;
padding: 5px;
border-radius: 40px;
}
.protocol-btn {
flex: 1;
border: none;
background: transparent;
padding: 10px;
border-radius: 40px;
font-weight: 600;
font-size: 14px;
color: #385073;
cursor: pointer;
}
.protocol-btn.active {
background: white;
color: #0b1c34;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.share-link {
background: #e8f0fe;
padding: 15px;
border-radius: 16px;
margin-top: 15px;
word-break: break-all;
font-size: 13px;
border: 1px solid #3b82f6;
}
.share-link a {
color: #3b82f6;
text-decoration: none;
font-weight: 600;
}
.log-entry {
padding: 6px;
border-bottom: 1px solid #dee9f5;
font-size: 12px;
color: #4f7092;
}
.status-badge {
display: inline-block;
padding: 4px 8px;
border-radius: 20px;
font-size: 11px;
font-weight: 600;
margin-left: 8px;
}
.status-badge.success {
background: #d1fae5;
color: #065f46;
}
.status-badge.error {
background: #fee2e2;
color: #b91c1c;
}
.status-badge.warning {
background: #fef3c7;
color: #92400e;
}
.json-view {
background: #1e293b;
color: #e2e8f0;
padding: 12px;
border-radius: 12px;
font-family: monospace;
font-size: 11px;
overflow-x: auto;
white-space: pre-wrap;
margin-top: 10px;
}
.protocol-info {
background: #dbeafe;
padding: 12px;
border-radius: 12px;
margin: 10px 0;
font-size: 13px;
border-left: 4px solid #3b82f6;
}
</style>
</head>
<body>
<div class="container">
<!-- Заголовок -->
<div class="card">
<h1>📱 Rabota.Today Mobile Debug</h1>
<div class="protocol-info" id="protocolInfo">
<strong>🔄 Определение протокола:</strong> Загрузка...
</div>
</div>
<!-- Информация об устройстве -->
<div class="card">
<h2>📱 Информация об устройстве</h2>
<div class="info-row">
<div class="label">🌐 Текущий протокол:</div>
<div class="value" id="currentProtocol"></div>
</div>
<div class="info-row">
<div class="label">🔗 Базовый URL API:</div>
<div class="value" id="baseApiUrl"></div>
</div>
<div class="info-row">
<div class="label">📱 Устройство:</div>
<div class="value" id="deviceInfo"></div>
</div>
<div class="info-row">
<div class="label">🌍 Текущий URL:</div>
<div class="value" id="currentUrl"></div>
</div>
<div class="info-row">
<div class="label">📶 Статус сети:</div>
<div class="value" id="networkStatus"></div>
</div>
</div>
<!-- Выбор протокола -->
<div class="card">
<h2>🔧 Настройки подключения</h2>
<div class="protocol-selector" id="protocolSelector">
<button class="protocol-btn active" data-protocol="auto" onclick="setProtocol('auto')">🔄 Авто</button>
<button class="protocol-btn" data-protocol="https" onclick="setProtocol('https')">🔒 HTTPS</button>
<button class="protocol-btn" data-protocol="http" onclick="setProtocol('http')">⚠️ HTTP</button>
</div>
<div class="flex-start" style="margin-top: 10px;">
<button class="secondary small" onclick="copyDebugInfo()">📋 Копировать информацию</button>
<button class="secondary small" onclick="saveIp()">💾 Сохранить настройки</button>
</div>
</div>
<!-- Основные тесты -->
<div class="card">
<h2>🔍 Диагностика API</h2>
<button onclick="runAllTests()" id="runAllBtn">▶️ Запустить все тесты</button>
<div class="flex" style="margin: 10px 0;">
<button class="secondary" onclick="testConnection()" style="flex: 1;">🔌 Тест соединения</button>
<button class="secondary" onclick="testCORS()" style="flex: 1;">🌐 Тест CORS</button>
</div>
<div class="flex" style="margin: 10px 0;">
<button class="secondary" onclick="testHealth()" style="flex: 1;">🏥 Health API</button>
<button class="secondary" onclick="testRegister()" style="flex: 1;">📝 Тест регистрации</button>
</div>
<div class="flex" style="margin: 10px 0;">
<button class="secondary" onclick="testLogin()" style="flex: 1;">🔑 Тест входа</button>
<button class="secondary" onclick="testAuth()" style="flex: 1;">👤 Тест авторизации</button>
</div>
<div id="mainResult" class="test-result"></div>
</div>
<!-- Результаты тестов -->
<div class="card" id="resultsCard" style="display: none;">
<h2>📊 Полные результаты</h2>
<div id="results" class="json-view"></div>
<div id="shareLink" class="share-link" style="display: none;">
<div>🔗 Результаты готовы:</div>
<textarea id="resultsText" style="width: 100%; height: 100px; margin: 10px 0; padding: 8px; border-radius: 8px; border: 1px solid #3b82f6;" readonly></textarea>
<button onclick="copyResults()" class="secondary">📋 Копировать результаты</button>
</div>
</div>
<!-- Лог ошибок -->
<div class="card">
<h2>📝 Лог событий</h2>
<div id="log" style="max-height: 200px; overflow-y: auto; font-size: 12px; background: #f5f5f5; padding: 10px; border-radius: 12px;">
<div class="log-entry">✅ Диагностика запущена</div>
</div>
<div class="flex" style="margin-top: 10px;">
<button class="secondary small" onclick="clearLog()">Очистить лог</button>
<button class="secondary small" onclick="downloadLog()">📥 Скачать лог</button>
</div>
</div>
<!-- Информация о помощи -->
<div class="card">
<h2>🆘 Помощь</h2>
<div class="info-row">
<div class="label">Если тесты не работают:</div>
<ul style="margin-left: 20px; margin-top: 5px;">
<li>Проверьте, что сервер запущен</li>
<li>Убедитесь, что вы в одной сети</li>
<li>Проверьте firewall (порт 8000)</li>
<li>Попробуйте HTTP вместо HTTPS</li>
</ul>
</div>
<div class="info-row">
<div class="label">Быстрые ссылки:</div>
<div class="flex-start">
<a href="/" class="secondary small" style="padding: 8px 12px;">🏠 Главная</a>
<a href="/register" class="secondary small" style="padding: 8px 12px;">📝 Регистрация</a>
<a href="/login" class="secondary small" style="padding: 8px 12px;">🔑 Вход</a>
</div>
</div>
</div>
</div>
<script>
// ==================== КОНФИГУРАЦИЯ ====================
// Определяем базовый URL динамически
const currentProtocol = window.location.protocol; // http: или https:
const currentHost = window.location.host; // yarmarka.rabota.today или IP:порт
let API_BASE_URL = `${currentProtocol}//${currentHost}/api`;
// Хранилище результатов
let testResults = {
device: {},
connection: [],
health: {},
cors: {},
register: {},
login: {},
auth: {},
timestamp: new Date().toISOString()
};
let currentProtocolMode = 'auto'; // auto, https, http
// ==================== ИНИЦИАЛИЗАЦИЯ ====================
function init() {
// Обновляем информацию
updateDeviceInfo();
updateProtocolInfo();
// Загружаем сохраненные настройки
const savedMode = localStorage.getItem('protocolMode');
if (savedMode) {
setProtocol(savedMode);
}
addLog('✅ Диагностика инициализирована');
addLog(`📡 API URL: ${API_BASE_URL}`);
}
function updateDeviceInfo() {
const info = {
userAgent: navigator.userAgent,
platform: navigator.platform,
language: navigator.language,
screen: `${window.screen.width}x${window.screen.height}`,
online: navigator.onLine,
vendor: navigator.vendor,
cookieEnabled: navigator.cookieEnabled
};
document.getElementById('deviceInfo').textContent =
`${info.platform}, ${info.screen}, ${info.language}`;
document.getElementById('currentUrl').textContent = window.location.href;
document.getElementById('currentProtocol').textContent = currentProtocol;
document.getElementById('baseApiUrl').textContent = API_BASE_URL;
document.getElementById('networkStatus').textContent =
info.online ? '✅ Онлайн' : '❌ Офлайн';
testResults.device = info;
}
function updateProtocolInfo() {
const protocolInfo = document.getElementById('protocolInfo');
const isHttps = currentProtocol === 'https:';
if (isHttps) {
protocolInfo.innerHTML = `
<strong>🔄 Текущий протокол: HTTPS (защищенный)</strong><br>
<span class="info">⚠️ API запросы должны идти через HTTPS или относительные пути</span>
`;
} else {
protocolInfo.innerHTML = `
<strong>🔄 Текущий протокол: HTTP (незащищенный)</strong><br>
<span class="success">✅ API запросы будут работать напрямую</span>
`;
}
}
// ==================== УПРАВЛЕНИЕ ПРОТОКОЛОМ ====================
function setProtocol(mode) {
currentProtocolMode = mode;
// Обновляем кнопки
document.querySelectorAll('.protocol-btn').forEach(btn => {
btn.classList.remove('active');
if (btn.dataset.protocol === mode) {
btn.classList.add('active');
}
});
// Сохраняем настройку
localStorage.setItem('protocolMode', mode);
// Перестраиваем API URL в зависимости от режима
if (mode === 'https') {
API_BASE_URL = `https://${currentHost}/api`;
} else if (mode === 'http') {
// Для HTTP нужно убедиться, что есть порт 8000
const hostWithoutPort = currentHost.split(':')[0];
API_BASE_URL = `http://${hostWithoutPort}:8000/api`;
} else {
// Auto - используем текущий протокол
API_BASE_URL = `${currentProtocol}//${currentHost}/api`;
}
document.getElementById('baseApiUrl').textContent = API_BASE_URL;
addLog(`🔄 Протокол изменен на ${mode}, API URL: ${API_BASE_URL}`);
}
// ==================== ЛОГИРОВАНИЕ ====================
function addLog(message, type = 'info') {
const logDiv = document.getElementById('log');
const entry = document.createElement('div');
entry.className = 'log-entry';
let icon = '📌';
if (type === 'success') icon = '✅';
if (type === 'error') icon = '❌';
if (type === 'warning') icon = '⚠️';
entry.textContent = `[${new Date().toLocaleTimeString()}] ${icon} ${message}`;
logDiv.insertBefore(entry, logDiv.firstChild);
// Ограничиваем количество записей
if (logDiv.children.length > 20) {
logDiv.removeChild(logDiv.lastChild);
}
}
function clearLog() {
document.getElementById('log').innerHTML = '<div class="log-entry">✅ Лог очищен</div>';
}
function downloadLog() {
const logEntries = [];
document.querySelectorAll('#log .log-entry').forEach(entry => {
logEntries.push(entry.textContent);
});
const logText = logEntries.join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `debug-log-${new Date().toISOString()}.txt`;
a.click();
}
// ==================== ОТОБРАЖЕНИЕ РЕЗУЛЬТАТОВ ====================
function showResult(title, content, isSuccess = true) {
const resultDiv = document.getElementById('mainResult');
resultDiv.style.display = 'block';
resultDiv.innerHTML = `
<div class="${isSuccess ? 'success' : 'error'}" style="margin-bottom: 10px;">
${isSuccess ? '✅' : '❌'} ${title}
</div>
<pre style="white-space: pre-wrap; font-size: 12px; background: white; padding: 10px; border-radius: 8px;">${JSON.stringify(content, null, 2)}</pre>
`;
}
function updateFullResults() {
const resultsDiv = document.getElementById('results');
const resultsText = document.getElementById('resultsText');
const resultsCard = document.getElementById('resultsCard');
resultsCard.style.display = 'block';
const resultsJson = JSON.stringify(testResults, null, 2);
resultsDiv.textContent = resultsJson;
resultsText.value = resultsJson;
document.getElementById('shareLink').style.display = 'block';
}
function copyResults() {
const resultsText = document.getElementById('resultsText');
resultsText.select();
document.execCommand('copy');
addLog('✅ Результаты скопированы');
alert('Результаты скопированы в буфер обмена!');
}
function copyDebugInfo() {
const info = {
url: window.location.href,
protocol: currentProtocol,
apiUrl: API_BASE_URL,
device: testResults.device,
timestamp: new Date().toISOString()
};
const textarea = document.createElement('textarea');
textarea.value = JSON.stringify(info, null, 2);
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
addLog('✅ Информация скопирована');
}
function saveIp() {
addLog('✅ Настройки сохранены');
alert('Настройки сохранены');
}
// ==================== ТЕСТЫ API ====================
async function testConnection() {
addLog('🔄 Тест соединения...');
const urls = [
`${API_BASE_URL}/health`,
`${API_BASE_URL.replace('/api', '')}/health`,
`${window.location.protocol}//${window.location.host}/api/health`,
`http://${window.location.hostname}:8000/api/health`
];
let results = [];
for (const url of urls) {
try {
const start = Date.now();
const response = await fetch(url, {
method: 'GET',
headers: { 'Accept': 'application/json' },
mode: 'cors'
});
const time = Date.now() - start;
let data = null;
try {
data = await response.json();
} catch {
data = await response.text();
}
const result = {
url,
status: response.status,
time: `${time}ms`,
ok: response.ok,
data: data
};
results.push(result);
if (response.ok) {
addLog(`✅ Доступно: ${url} (${time}ms)`);
} else {
addLog(`⚠️ Ошибка ${response.status}: ${url}`);
}
} catch (e) {
results.push({
url,
error: e.message,
ok: false
});
addLog(`❌ Недоступно: ${url} - ${e.message}`, 'error');
}
}
testResults.connection = results;
// Проверяем, есть ли успешные подключения
const hasSuccess = results.some(r => r.ok);
showResult('Тест соединения', results, hasSuccess);
return results;
}
async function testHealth() {
addLog('🔄 Тест Health API...');
try {
const url = `${API_BASE_URL}/health`;
const response = await fetch(url, {
headers: { 'Accept': 'application/json' }
});
let data;
try {
data = await response.json();
} catch {
data = await response.text();
}
testResults.health = {
status: response.status,
ok: response.ok,
data,
url
};
if (response.ok) {
addLog('✅ Health API работает');
showResult('Health API', data, true);
} else {
addLog(`❌ Health API ошибка: ${response.status}`);
showResult('Health API ошибка', { status: response.status, data }, false);
}
return { ok: response.ok, data };
} catch (error) {
testResults.health = { error: error.message };
addLog(`❌ Health API: ${error.message}`, 'error');
showResult('Health API ошибка', { error: error.message }, false);
return { ok: false, error: error.message };
}
}
async function testCORS() {
addLog('🔄 Тест CORS...');
try {
const url = `${API_BASE_URL}/health`;
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
}
});
const corsHeader = response.headers.get('access-control-allow-origin');
const corsCredentials = response.headers.get('access-control-allow-credentials');
testResults.cors = {
allowed: corsHeader === '*' || corsHeader === window.location.origin,
header: corsHeader,
credentials: corsCredentials,
url: url
};
if (corsHeader) {
addLog(`✅ CORS заголовок: ${corsHeader}`);
showResult('CORS тест', testResults.cors, true);
} else {
addLog('⚠️ Нет CORS заголовка', 'warning');
showResult('CORS предупреждение', testResults.cors, false);
}
} catch (error) {
testResults.cors = { error: error.message };
addLog(`❌ CORS тест: ${error.message}`, 'error');
showResult('CORS ошибка', { error: error.message }, false);
}
}
async function testRegister() {
addLog('🔄 Тест регистрации...');
const testData = {
full_name: "Тест Тестов",
email: `test${Date.now()}@example.com`,
phone: "+79991234567",
telegram: null,
password: "password123",
role: "employee"
};
try {
const url = `${API_BASE_URL}/register`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(testData)
});
let data;
try {
data = await response.json();
} catch {
data = await response.text();
}
testResults.register = {
status: response.status,
ok: response.ok,
data: data,
url: url
};
if (response.ok) {
addLog(`✅ Регистрация работает, создан пользователь ID: ${data.user_id}`);
showResult('Регистрация', {
success: true,
user_id: data.user_id,
email: testData.email
}, true);
} else {
addLog(`❌ Регистрация ошибка: ${response.status}`);
showResult('Ошибка регистрации', { status: response.status, data }, false);
}
return { ok: response.ok, data };
} catch (error) {
testResults.register = { error: error.message };
addLog(`❌ Регистрация: ${error.message}`, 'error');
showResult('Ошибка регистрации', { error: error.message }, false);
return { ok: false, error: error.message };
}
}
async function testLogin() {
addLog('🔄 Тест входа...');
try {
const url = `${API_BASE_URL}/login`;
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify({
email: "admin@rabota.today",
password: "admin123"
})
});
let data;
try {
data = await response.json();
} catch {
data = await response.text();
}
testResults.login = {
status: response.status,
ok: response.ok,
data: data,
url: url
};
if (response.ok) {
addLog(`✅ Вход работает, получен токен для ${data.full_name}`);
showResult('Вход в систему', {
success: true,
user: data.full_name,
role: data.role
}, true);
// Сохраняем токен для теста авторизации
if (data.access_token) {
localStorage.setItem('test_token', data.access_token);
}
} else {
addLog(`❌ Вход ошибка: ${response.status}`);
showResult('Ошибка входа', { status: response.status, data }, false);
}
return { ok: response.ok, data };
} catch (error) {
testResults.login = { error: error.message };
addLog(`❌ Вход: ${error.message}`, 'error');
showResult('Ошибка входа', { error: error.message }, false);
return { ok: false, error: error.message };
}
}
async function testAuth() {
addLog('🔄 Тест авторизации...');
const token = localStorage.getItem('test_token');
if (!token) {
addLog('⚠️ Нет токена для теста авторизации', 'warning');
showResult('Тест авторизации', { error: 'Сначала выполните вход' }, false);
return;
}
try {
const url = `${API_BASE_URL}/user`;
const response = await fetch(url, {
headers: {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json'
}
});
let data;
try {
data = await response.json();
} catch {
data = await response.text();
}
testResults.auth = {
status: response.status,
ok: response.ok,
data: data
};
if (response.ok) {
addLog(`✅ Авторизация работает, пользователь: ${data.full_name}`);
showResult('Авторизация', {
success: true,
user: data.full_name,
email: data.email
}, true);
} else {
addLog(`❌ Авторизация ошибка: ${response.status}`);
showResult('Ошибка авторизации', { status: response.status, data }, false);
}
} catch (error) {
testResults.auth = { error: error.message };
addLog(`❌ Авторизация: ${error.message}`, 'error');
showResult('Ошибка авторизации', { error: error.message }, false);
}
}
async function runAllTests() {
const btn = document.getElementById('runAllBtn');
btn.disabled = true;
btn.innerHTML = '<div class="loader"></div> Выполнение тестов...';
// Очищаем предыдущие результаты
testResults = {
device: testResults.device,
timestamp: new Date().toISOString()
};
addLog('🚀 Запуск всех тестов...');
// Последовательное выполнение тестов
await testConnection();
await testHealth();
await testCORS();
await testRegister();
await testLogin();
await testAuth();
// Обновляем полные результаты
updateFullResults();
btn.disabled = false;
btn.innerHTML = '▶️ Запустить все тесты';
addLog('✅ Все тесты завершены');
}
// ==================== ЗАПУСК ====================
// Запускаем инициализацию при загрузке
window.addEventListener('load', () => {
init();
// Автоматически запускаем базовые тесты через 1 секунду
setTimeout(() => {
testConnection();
}, 1000);
});
</script>
</body>
</html>