feat: frontend & backend connectivity
This commit is contained in:
@@ -233,10 +233,10 @@ app.delete('/notes/:id', function(request, response) {
|
||||
});
|
||||
});
|
||||
|
||||
// РОУТ 6: Корневой маршрут (для проверки работы сервера)
|
||||
// РОУТ 7: Корневой маршрут (для проверки работы сервера)
|
||||
// Метод: GET
|
||||
// Адрес: /
|
||||
app.get('/', function(request, response) {
|
||||
app.get('/', function(_, response) {
|
||||
/*
|
||||
Простой маршрут для проверки, что сервер работает
|
||||
*/
|
||||
|
||||
5
frontend/src/public/config.json
Normal file
5
frontend/src/public/config.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"backendUrl": "http://localhost:3000",
|
||||
"apiPrefix": "",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
@@ -3,10 +3,318 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Document</title>
|
||||
<title>Проверка связи с Backend</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
color: #333;
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.header p {
|
||||
color: #666;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.status-card {
|
||||
background: #f8f9fa;
|
||||
border-radius: 15px;
|
||||
padding: 30px;
|
||||
margin-bottom: 30px;
|
||||
border-left: 5px solid #6c757d;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.status-card.active {
|
||||
border-left-color: #28a745;
|
||||
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||
}
|
||||
|
||||
.status-card.error {
|
||||
border-left-color: #dc3545;
|
||||
background: linear-gradient(135deg, #f8d7da 0%, #f5c6cb 100%);
|
||||
}
|
||||
|
||||
.status-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.status-title {
|
||||
font-size: 1.3rem;
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.status-details {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.detail-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
color: #333;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-weight: 600;
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.detail-value.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.detail-value.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 15px 25px;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #f8f9fa;
|
||||
color: #333;
|
||||
border: 2px solid #dee2e6;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e9ecef;
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid rgba(255,255,255,.3);
|
||||
border-radius: 50%;
|
||||
border-top-color: white;
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.log-container {
|
||||
margin-top: 30px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.log-title {
|
||||
color: #333;
|
||||
font-weight: 600;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.log-entry {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 0.9rem;
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.log-entry:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.log-entry.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.log-entry.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.log-entry.info {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.timestamp {
|
||||
color: #6c757d;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.config-warning {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffeaa7;
|
||||
border-radius: 10px;
|
||||
padding: 15px;
|
||||
margin-bottom: 20px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>О нас</h1>
|
||||
<p>Мы разработали это приложение... бесплатно.</p>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔍 Проверка связи</h1>
|
||||
<p>Мониторинг подключения к Backend серверу</p>
|
||||
</div>
|
||||
|
||||
<div class="config-warning" id="configWarning" style="display: none;">
|
||||
⚠️ Конфигурация загружена с запасного URL. Проверьте подключение.
|
||||
</div>
|
||||
|
||||
<div class="status-card" id="statusCard">
|
||||
<div class="status-info">
|
||||
<div class="status-title">Статус Backend сервера</div>
|
||||
<div class="status-indicator offline" id="statusIndicator">Проверка...</div>
|
||||
</div>
|
||||
|
||||
<div class="status-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Backend URL:</span>
|
||||
<span class="detail-value" id="backendUrl">Загрузка...</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Статус:</span>
|
||||
<span class="detail-value" id="backendStatus">Проверяется...</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Время ответа:</span>
|
||||
<span class="detail-value" id="responseTime">—</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">Последняя проверка:</span>
|
||||
<span class="detail-value" id="lastCheck">—</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="btn btn-primary" id="checkBtn" onclick="checkBackend()">
|
||||
<span>🔄 Проверить соединение</span>
|
||||
</button>
|
||||
<button class="btn btn-secondary" id="autoCheckBtn" onclick="toggleAutoCheck()">
|
||||
<span>⏱️ Автопроверка: ВЫКЛ</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="log-container">
|
||||
<div class="log-title">Журнал проверок:</div>
|
||||
<div id="logEntries"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="/js/status.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
318
frontend/src/public/js/status.js
Normal file
318
frontend/src/public/js/status.js
Normal file
@@ -0,0 +1,318 @@
|
||||
class BackendMonitor {
|
||||
constructor() {
|
||||
this.config = null;
|
||||
this.isChecking = false;
|
||||
this.autoCheckInterval = null;
|
||||
this.autoCheckEnabled = false;
|
||||
this.checkInterval = 30000; // 30 секунд
|
||||
this.logs = [];
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.loadConfig();
|
||||
await this.checkBackend();
|
||||
|
||||
// Начинаем автопроверку если в конфиге указано
|
||||
if (this.config?.autoCheck === true) {
|
||||
this.enableAutoCheck();
|
||||
}
|
||||
}
|
||||
|
||||
async loadConfig() {
|
||||
try {
|
||||
// Пробуем загрузить конфиг с сервера
|
||||
const response = await fetch('/config.json');
|
||||
if (!response.ok) throw new Error('Failed to load config');
|
||||
|
||||
this.config = await response.json();
|
||||
this.updateUI('configLoaded');
|
||||
|
||||
} catch (error) {
|
||||
console.warn('Не удалось загрузить конфигурацию:', error);
|
||||
|
||||
// Используем запасную конфигурацию
|
||||
this.config = {
|
||||
backendUrl: 'http://localhost:3000',
|
||||
healthCheckEndpoint: '/',
|
||||
timeout: 5000,
|
||||
environment: 'fallback',
|
||||
autoCheck: false
|
||||
};
|
||||
|
||||
this.showConfigWarning();
|
||||
this.updateUI('configFallback');
|
||||
}
|
||||
}
|
||||
|
||||
async checkBackend() {
|
||||
if (this.isChecking) return;
|
||||
|
||||
this.isChecking = true;
|
||||
this.updateUI('checking');
|
||||
|
||||
const startTime = Date.now();
|
||||
let isSuccess = false;
|
||||
let responseTime = null;
|
||||
let errorMessage = null;
|
||||
let details = {};
|
||||
|
||||
try {
|
||||
// Проверяем доступность сервера
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout || 10000);
|
||||
|
||||
const response = await fetch(this.config.backendUrl + (this.config.healthCheckEndpoint || ''), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
responseTime = Date.now() - startTime;
|
||||
|
||||
if (response.ok) {
|
||||
isSuccess = true;
|
||||
try {
|
||||
const data = await response.json();
|
||||
details = data;
|
||||
} catch (e) {
|
||||
details = { message: 'Сервер доступен' };
|
||||
}
|
||||
} else {
|
||||
errorMessage = `HTTP ${response.status}: ${response.statusText}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
responseTime = Date.now() - startTime;
|
||||
errorMessage = this.getErrorMessage(error);
|
||||
}
|
||||
|
||||
this.isChecking = false;
|
||||
|
||||
// Записываем лог
|
||||
this.addLog(isSuccess, responseTime, errorMessage, details);
|
||||
|
||||
// Обновляем UI
|
||||
this.updateUI(isSuccess ? 'success' : 'error', {
|
||||
responseTime,
|
||||
errorMessage,
|
||||
details
|
||||
});
|
||||
}
|
||||
|
||||
getErrorMessage(error) {
|
||||
if (error.name === 'AbortError') {
|
||||
return 'Таймаут запроса';
|
||||
}
|
||||
if (error.message.includes('Failed to fetch')) {
|
||||
return 'Сеть недоступна';
|
||||
}
|
||||
if (error.message.includes('net::ERR_CONNECTION_REFUSED')) {
|
||||
return 'Соединение отклонено';
|
||||
}
|
||||
return error.message || 'Неизвестная ошибка';
|
||||
}
|
||||
|
||||
addLog(isSuccess, responseTime, errorMessage, details) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const logEntry = {
|
||||
timestamp,
|
||||
success: isSuccess,
|
||||
responseTime,
|
||||
errorMessage,
|
||||
details,
|
||||
type: isSuccess ? 'success' : 'error'
|
||||
};
|
||||
|
||||
this.logs.unshift(logEntry); // Добавляем в начало
|
||||
|
||||
// Храним только последние 10 записей
|
||||
if (this.logs.length > 10) {
|
||||
this.logs.pop();
|
||||
}
|
||||
|
||||
this.updateLogsUI();
|
||||
}
|
||||
|
||||
updateLogsUI() {
|
||||
const logContainer = document.getElementById('logEntries');
|
||||
if (!logContainer) return;
|
||||
|
||||
logContainer.innerHTML = this.logs.map(log => `
|
||||
<div class="log-entry ${log.type}">
|
||||
<span class="timestamp">[${log.timestamp}]</span>
|
||||
${log.success
|
||||
? `✅ Успешно (${log.responseTime}ms)`
|
||||
: `❌ Ошибка: ${log.errorMessage}`
|
||||
}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
updateUI(state, data = {}) {
|
||||
const statusCard = document.getElementById('statusCard');
|
||||
const statusIndicator = document.getElementById('statusIndicator');
|
||||
const backendUrl = document.getElementById('backendUrl');
|
||||
const backendStatus = document.getElementById('backendStatus');
|
||||
const responseTime = document.getElementById('responseTime');
|
||||
const lastCheck = document.getElementById('lastCheck');
|
||||
const checkBtn = document.getElementById('checkBtn');
|
||||
|
||||
if (!statusCard) return;
|
||||
|
||||
switch(state) {
|
||||
case 'configLoaded':
|
||||
backendUrl.textContent = this.config.backendUrl;
|
||||
backendUrl.className = 'detail-value success';
|
||||
break;
|
||||
|
||||
case 'configFallback':
|
||||
backendUrl.textContent = `${this.config.backendUrl} (запасной)`;
|
||||
backendUrl.className = 'detail-value error';
|
||||
break;
|
||||
|
||||
case 'checking':
|
||||
statusCard.className = 'status-card';
|
||||
statusIndicator.textContent = 'Проверка...';
|
||||
statusIndicator.className = 'status-indicator offline';
|
||||
backendStatus.textContent = 'Выполняется проверка...';
|
||||
backendStatus.className = 'detail-value';
|
||||
checkBtn.disabled = true;
|
||||
checkBtn.innerHTML = '<span class="loading"></span> Проверка...';
|
||||
break;
|
||||
|
||||
case 'success':
|
||||
statusCard.className = 'status-card active';
|
||||
statusIndicator.textContent = 'Online';
|
||||
statusIndicator.className = 'status-indicator online';
|
||||
backendStatus.textContent = '✅ Сервер доступен';
|
||||
backendStatus.className = 'detail-value success';
|
||||
responseTime.textContent = `${data.responseTime} ms`;
|
||||
lastCheck.textContent = new Date().toLocaleTimeString();
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.innerHTML = '🔄 Проверить соединение';
|
||||
break;
|
||||
|
||||
case 'error':
|
||||
statusCard.className = 'status-card error';
|
||||
statusIndicator.textContent = 'Offline';
|
||||
statusIndicator.className = 'status-indicator offline';
|
||||
backendStatus.textContent = `❌ ${data.errorMessage}`;
|
||||
backendStatus.className = 'detail-value error';
|
||||
responseTime.textContent = data.responseTime ? `${data.responseTime} ms` : '—';
|
||||
lastCheck.textContent = new Date().toLocaleTimeString();
|
||||
checkBtn.disabled = false;
|
||||
checkBtn.innerHTML = '🔄 Проверить соединение';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
showConfigWarning() {
|
||||
const warning = document.getElementById('configWarning');
|
||||
if (warning) {
|
||||
warning.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
toggleAutoCheck() {
|
||||
if (this.autoCheckEnabled) {
|
||||
this.disableAutoCheck();
|
||||
} else {
|
||||
this.enableAutoCheck();
|
||||
}
|
||||
}
|
||||
|
||||
enableAutoCheck() {
|
||||
this.autoCheckEnabled = true;
|
||||
this.autoCheckInterval = setInterval(() => {
|
||||
this.checkBackend();
|
||||
}, this.checkInterval);
|
||||
|
||||
const btn = document.getElementById('autoCheckBtn');
|
||||
if (btn) {
|
||||
btn.innerHTML = '⏸️ Автопроверка: ВКЛ';
|
||||
}
|
||||
|
||||
this.addLog(false, null, null, { type: 'info', message: 'Автопроверка включена' });
|
||||
}
|
||||
|
||||
disableAutoCheck() {
|
||||
this.autoCheckEnabled = false;
|
||||
if (this.autoCheckInterval) {
|
||||
clearInterval(this.autoCheckInterval);
|
||||
this.autoCheckInterval = null;
|
||||
}
|
||||
|
||||
const btn = document.getElementById('autoCheckBtn');
|
||||
if (btn) {
|
||||
btn.innerHTML = '⏱️ Автопроверка: ВЫКЛ';
|
||||
}
|
||||
|
||||
this.addLog(false, null, null, { type: 'info', message: 'Автопроверка выключена' });
|
||||
}
|
||||
|
||||
// Дополнительные проверки
|
||||
async runExtendedTests() {
|
||||
const tests = [
|
||||
{ name: 'Пинг сервера', endpoint: '' },
|
||||
{ name: 'API Health', endpoint: this.config.healthCheckEndpoint || '/' },
|
||||
{ name: 'Версия API', endpoint: '/api/version' }
|
||||
];
|
||||
|
||||
const results = [];
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const start = Date.now();
|
||||
const response = await fetch(this.config.backendUrl + test.endpoint, {
|
||||
signal: AbortSignal.timeout(5000)
|
||||
});
|
||||
const time = Date.now() - start;
|
||||
|
||||
results.push({
|
||||
name: test.name,
|
||||
success: response.ok,
|
||||
time,
|
||||
status: response.status
|
||||
});
|
||||
} catch (error) {
|
||||
results.push({
|
||||
name: test.name,
|
||||
success: false,
|
||||
time: null,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
}
|
||||
|
||||
// Глобальные функции для вызова из HTML
|
||||
let monitor;
|
||||
|
||||
function checkBackend() {
|
||||
if (!monitor) {
|
||||
monitor = new BackendMonitor();
|
||||
} else {
|
||||
monitor.checkBackend();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoCheck() {
|
||||
if (!monitor) {
|
||||
monitor = new BackendMonitor();
|
||||
}
|
||||
monitor.toggleAutoCheck();
|
||||
}
|
||||
|
||||
// Инициализация при загрузке страницы
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
monitor = new BackendMonitor();
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import fs from 'fs';
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.FRONTEND_PORT || 8080;
|
||||
|
||||
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:3000';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
@@ -14,6 +14,14 @@ const __dirname = dirname(__filename);
|
||||
// файлу в public, то буден выдан данный файл
|
||||
app.use(express.static(join(__dirname, 'public')));
|
||||
|
||||
// Конфигурационный endpoint
|
||||
app.get('/config.json', (_, res) => {
|
||||
res.json({
|
||||
backendUrl: BACKEND_URL,
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
});
|
||||
});
|
||||
|
||||
// Запросы на / выдают index.html по умолчанию
|
||||
app.get('/', function(_, res) {
|
||||
res.sendFile(join(__dirname, 'public', 'html', 'index.html'));
|
||||
|
||||
Reference in New Issue
Block a user