All checks were successful
continuous-integration/drone/push Build is passing
Changes: - Fix nutrition service: add is_active column and Pydantic validation for UUID/datetime - Add location-based alerts feature: users can now see alerts within 1km radius - Fix CORS and response serialization in nutrition service - Add getCurrentLocation() and loadAlertsNearby() functions - Improve UI for nearby alerts display with distance and response count
1144 lines
43 KiB
HTML
1144 lines
43 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Women's Safety App - API Tester</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;
|
||
padding: 20px;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
color: white;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||
}
|
||
|
||
header p {
|
||
font-size: 1.1em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.main-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 2fr;
|
||
gap: 20px;
|
||
margin-bottom: 30px;
|
||
}
|
||
|
||
.panel {
|
||
background: white;
|
||
border-radius: 10px;
|
||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||
padding: 20px;
|
||
}
|
||
|
||
.panel h2 {
|
||
color: #667eea;
|
||
margin-bottom: 15px;
|
||
border-bottom: 2px solid #667eea;
|
||
padding-bottom: 10px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
input, textarea, select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
font-size: 1em;
|
||
font-family: inherit;
|
||
}
|
||
|
||
input:focus, textarea:focus, select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 5px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
button {
|
||
width: 100%;
|
||
padding: 10px;
|
||
margin-top: 10px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-size: 1em;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
button:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
button:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.status {
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
text-align: center;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.status.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 1px solid #c3e6cb;
|
||
}
|
||
|
||
.status.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 1px solid #f5c6cb;
|
||
}
|
||
|
||
.status.info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border: 1px solid #bee5eb;
|
||
}
|
||
|
||
.results {
|
||
background: #f8f9fa;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
padding: 15px;
|
||
margin-top: 15px;
|
||
max-height: 400px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.tab-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab-button {
|
||
flex: 1;
|
||
min-width: 150px;
|
||
padding: 12px;
|
||
background: #f0f0f0;
|
||
border: 2px solid transparent;
|
||
border-radius: 5px;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab-button.active {
|
||
background: #667eea;
|
||
color: white;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.tab-button:hover {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.endpoint-list {
|
||
list-style: none;
|
||
}
|
||
|
||
.endpoint-list li {
|
||
padding: 10px;
|
||
margin-bottom: 10px;
|
||
background: #f8f9fa;
|
||
border-left: 4px solid #667eea;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.endpoint-list .method {
|
||
display: inline-block;
|
||
padding: 3px 8px;
|
||
border-radius: 3px;
|
||
color: white;
|
||
font-weight: 600;
|
||
font-size: 0.85em;
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.endpoint-list .get {
|
||
background: #28a745;
|
||
}
|
||
|
||
.endpoint-list .post {
|
||
background: #007bff;
|
||
}
|
||
|
||
.endpoint-list .put {
|
||
background: #ffc107;
|
||
}
|
||
|
||
.endpoint-list .delete {
|
||
background: #dc3545;
|
||
}
|
||
|
||
.auth-info {
|
||
background: #e7f3ff;
|
||
border: 1px solid #b3d9ff;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
font-size: 0.9em;
|
||
}
|
||
|
||
.token-display {
|
||
word-break: break-all;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.8em;
|
||
background: #f5f5f5;
|
||
padding: 8px;
|
||
border-radius: 3px;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.main-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 2em;
|
||
}
|
||
|
||
.tab-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.tab-button {
|
||
min-width: auto;
|
||
}
|
||
}
|
||
|
||
.loader {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
border: 3px solid #f3f3f3;
|
||
border-top: 3px solid #667eea;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
0% { transform: rotate(0deg); }
|
||
100% { transform: rotate(360deg); }
|
||
}
|
||
|
||
.copy-btn {
|
||
padding: 5px 10px;
|
||
font-size: 0.8em;
|
||
width: auto;
|
||
margin-top: 0;
|
||
margin-left: 5px;
|
||
}
|
||
|
||
pre {
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
overflow-x: auto;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
/* WebSocket SOS Styles */
|
||
.ws-indicator {
|
||
display: inline-block;
|
||
width: 12px;
|
||
height: 12px;
|
||
border-radius: 50%;
|
||
margin-right: 8px;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.ws-indicator.connected {
|
||
background: #4caf50;
|
||
}
|
||
|
||
.ws-indicator.disconnected {
|
||
background: #f44336;
|
||
animation: none;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { opacity: 1; }
|
||
50% { opacity: 0.5; }
|
||
}
|
||
|
||
.ws-log {
|
||
background: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
height: 200px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.85em;
|
||
margin-top: 10px;
|
||
}
|
||
|
||
.log-entry {
|
||
padding: 5px;
|
||
margin: 3px 0;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.log-entry.info {
|
||
background: #e3f2fd;
|
||
color: #1565c0;
|
||
}
|
||
|
||
.log-entry.success {
|
||
background: #e8f5e9;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.log-entry.error {
|
||
background: #ffebee;
|
||
color: #c62828;
|
||
}
|
||
|
||
.log-entry.warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.stats-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 10px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.stat-box {
|
||
background: #f5f5f5;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
text-align: center;
|
||
border: 1px solid #ddd;
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 1.5em;
|
||
font-weight: bold;
|
||
color: #667eea;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.85em;
|
||
color: #666;
|
||
margin-top: 5px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>👩💼 Women's Safety App</h1>
|
||
<p>API Testing Platform</p>
|
||
</header>
|
||
|
||
<div class="main-grid">
|
||
<!-- Left Panel: Auth & Navigation -->
|
||
<div class="panel">
|
||
<h2>🔐 Аутентификация</h2>
|
||
|
||
<div class="tab-buttons" style="flex-direction: column;">
|
||
<button class="tab-button active" onclick="switchTab('auth-login')">Вход</button>
|
||
<button class="tab-button" onclick="switchTab('auth-register')">Регистрация</button>
|
||
<button class="tab-button" onclick="switchTab('endpoints')">Ендпоинты</button>
|
||
</div>
|
||
|
||
<!-- Login Tab -->
|
||
<div id="auth-login" class="tab-content active">
|
||
<div class="form-group">
|
||
<label>Имя пользователя</label>
|
||
<input type="text" id="login-username" placeholder="Введите имя пользователя">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Пароль</label>
|
||
<input type="password" id="login-password" placeholder="Введите пароль">
|
||
</div>
|
||
<button onclick="login()">🔓 Войти</button>
|
||
<div id="login-status"></div>
|
||
<div class="auth-info" id="token-info" style="display: none;">
|
||
<strong>✅ Авторизован!</strong>
|
||
<div class="token-display" id="token-display"></div>
|
||
<button class="copy-btn" onclick="copyToken()">📋 Копировать токен</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Register Tab -->
|
||
<div id="auth-register" class="tab-content">
|
||
<div class="form-group">
|
||
<label>Email</label>
|
||
<input type="email" id="reg-email" placeholder="user@example.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Имя пользователя</label>
|
||
<input type="text" id="reg-username" placeholder="Выберите имя">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Пароль</label>
|
||
<input type="password" id="reg-password" placeholder="Минимум 8 символов">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Полное имя (опционально)</label>
|
||
<input type="text" id="reg-fullname" placeholder="Ваше полное имя">
|
||
</div>
|
||
<button onclick="register()">✍️ Зарегистрироваться</button>
|
||
<div id="register-status"></div>
|
||
</div>
|
||
|
||
<!-- Endpoints Tab -->
|
||
<div id="endpoints" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 10px;">Основные ендпоинты:</h3>
|
||
<ul class="endpoint-list">
|
||
<li>
|
||
<span class="method get">GET</span>
|
||
<strong>/health</strong> - Health check
|
||
</li>
|
||
<li>
|
||
<span class="method post">POST</span>
|
||
<strong>/api/v1/auth/register</strong> - Регистрация
|
||
</li>
|
||
<li>
|
||
<span class="method post">POST</span>
|
||
<strong>/api/v1/auth/login</strong> - Вход
|
||
</li>
|
||
<li>
|
||
<span class="method get">GET</span>
|
||
<strong>/api/v1/profile</strong> - Профиль пользователя
|
||
</li>
|
||
<li>
|
||
<span class="method get">GET</span>
|
||
<strong>/alerts</strong> - Список алертов
|
||
</li>
|
||
<li>
|
||
<span class="method post">POST</span>
|
||
<strong>/alerts</strong> - Создать алерт
|
||
</li>
|
||
<li>
|
||
<span class="method get">GET</span>
|
||
<strong>/api/v1/events</strong> - События календаря
|
||
</li>
|
||
<li>
|
||
<span class="method post">POST</span>
|
||
<strong>/notify</strong> - Отправить уведомление
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Right Panel: API Testing -->
|
||
<div class="panel">
|
||
<h2>🧪 Тестирование API</h2>
|
||
|
||
<div class="tab-buttons">
|
||
<button class="tab-button active" onclick="switchTab('test-users')">👤 Пользователи</button>
|
||
<button class="tab-button" onclick="switchTab('test-alerts')">🚨 Алерты</button>
|
||
<button class="tab-button" onclick="switchTab('test-calendar')">📅 Календарь</button>
|
||
<button class="tab-button" onclick="switchTab('test-notify')">🔔 Уведомления</button>
|
||
<button class="tab-button" onclick="switchTab('test-websocket')">📡 WebSocket SOS</button>
|
||
<button class="tab-button" onclick="switchTab('test-health')">❤️ Health</button>
|
||
</div>
|
||
|
||
<!-- Users Tab -->
|
||
<div id="test-users" class="tab-content active">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">Управление пользователями</h3>
|
||
|
||
<button onclick="getProfile()">👤 Получить профиль</button>
|
||
<button onclick="getAllUsers()">📋 Получить всех пользователей</button>
|
||
<button onclick="updateProfile()">✏️ Обновить профиль</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Информация профиля:</label>
|
||
<div class="results" id="users-result"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Alerts Tab -->
|
||
<div id="test-alerts" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">Emergency Alerts</h3>
|
||
|
||
<div class="form-group">
|
||
<label>Широта (Latitude)</label>
|
||
<input type="number" id="alert-lat" value="40.7128" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Долгота (Longitude)</label>
|
||
<input type="number" id="alert-lon" value="-74.0060" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Тип алерта</label>
|
||
<select id="alert-type">
|
||
<option value="medical">Медицинское</option>
|
||
<option value="safety">Безопасность</option>
|
||
<option value="harassment">Преследование</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button onclick="getAlerts()">📖 Получить алерты</button>
|
||
<button onclick="createAlert()">🚨 Создать алерт</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Результат:</label>
|
||
<div class="results" id="alerts-result"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Calendar Tab -->
|
||
<div id="test-calendar" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">Календарь</h3>
|
||
|
||
<div class="form-group">
|
||
<label>Дата записи</label>
|
||
<input type="date" id="calendar-date">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Тип записи</label>
|
||
<select id="calendar-type">
|
||
<option value="period">Критические дни</option>
|
||
<option value="ovulation">Овуляция</option>
|
||
<option value="normal">Обычный день</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Самочувствие</label>
|
||
<select id="calendar-mood">
|
||
<option value="happy">😊 Хорошее</option>
|
||
<option value="normal">😐 Нормальное</option>
|
||
<option value="sad">😞 Плохое</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button onclick="getCalendarEvents()">📋 Получить события</button>
|
||
<button onclick="createCalendarEntry()">➕ Добавить запись</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Результат:</label>
|
||
<div class="results" id="calendar-result"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Notifications Tab -->
|
||
<div id="test-notify" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">Уведомления</h3>
|
||
|
||
<div class="form-group">
|
||
<label>Заголовок</label>
|
||
<input type="text" id="notify-title" placeholder="Тема уведомления">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Сообщение</label>
|
||
<textarea id="notify-message" placeholder="Текст уведомления" rows="3"></textarea>
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Приоритет</label>
|
||
<select id="notify-priority">
|
||
<option value="low">Низкий</option>
|
||
<option value="normal">Нормальный</option>
|
||
<option value="high">Высокий</option>
|
||
</select>
|
||
</div>
|
||
|
||
<button onclick="sendNotification()">📤 Отправить уведомление</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Результат:</label>
|
||
<div class="results" id="notify-result"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- WebSocket SOS Tab -->
|
||
<div id="test-websocket" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">
|
||
<span class="ws-indicator disconnected" id="ws-indicator"></span>
|
||
WebSocket SOS Тестирование
|
||
</h3>
|
||
|
||
<div style="background: #e3f2fd; padding: 10px; border-radius: 5px; margin-bottom: 15px; border-left: 4px solid #667eea;">
|
||
<strong>Статус:</strong> <span id="ws-status">Отключено</span>
|
||
</div>
|
||
|
||
<button onclick="wsConnect()">🔌 Подключиться</button>
|
||
<button onclick="wsDisconnect()">❌ Отключиться</button>
|
||
<button onclick="wsSendPing()">📤 Отправить Ping</button>
|
||
<button onclick="clearWSLog()">🗑️ Очистить логи</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Параметры SOS Alert:</label>
|
||
<input type="number" id="ws-lat" placeholder="Широта" value="55.7558" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<input type="number" id="ws-lon" placeholder="Долгота" value="37.6173" step="0.0001">
|
||
</div>
|
||
<div class="form-group">
|
||
<input type="text" id="ws-address" placeholder="Адрес" value="Тестовое место">
|
||
</div>
|
||
<div class="form-group">
|
||
<select id="ws-alert-type">
|
||
<option value="violence">🚨 Насилие</option>
|
||
<option value="medical">🏥 Медицинская помощь</option>
|
||
<option value="harassment">😠 Преследование</option>
|
||
<option value="unsafe_area">⚠️ Опасная зона</option>
|
||
<option value="accident">🚗 Авария</option>
|
||
<option value="fire">🔥 Пожар</option>
|
||
<option value="general">📍 Чрезвычайная ситуация</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<textarea id="ws-message" placeholder="Сообщение" rows="2">🚨 Тестовый WebSocket SOS Alert</textarea>
|
||
</div>
|
||
|
||
<button onclick="wsCreateSOS()" style="background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);">🚨 Создать SOS Alert</button>
|
||
|
||
<div class="stats-grid">
|
||
<div class="stat-box">
|
||
<div class="stat-value" id="ws-ping-count">0</div>
|
||
<div class="stat-label">Пингов отправлено</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-value" id="ws-pong-count">0</div>
|
||
<div class="stat-label">Понгов получено</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-value" id="ws-msg-count">0</div>
|
||
<div class="stat-label">Сообщений получено</div>
|
||
</div>
|
||
<div class="stat-box">
|
||
<div class="stat-value" id="ws-latency">--</div>
|
||
<div class="stat-label">Latency (ms)</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>📨 Логи WebSocket:</label>
|
||
<div class="ws-log" id="ws-log"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Health Tab -->
|
||
<div id="test-health" class="tab-content">
|
||
<h3 style="color: #667eea; margin-bottom: 15px;">Статус сервисов</h3>
|
||
|
||
<button onclick="checkAllHealth()">🔍 Проверить все сервисы</button>
|
||
<button onclick="checkGatewayHealth()">🌐 API Gateway</button>
|
||
<button onclick="checkUserServiceHealth()">👤 User Service</button>
|
||
<button onclick="checkEmergencyServiceHealth()">🚨 Emergency Service</button>
|
||
<button onclick="checkCalendarServiceHealth()">📅 Calendar Service</button>
|
||
<button onclick="checkNotificationServiceHealth()">🔔 Notification Service</button>
|
||
|
||
<div class="form-group" style="margin-top: 15px;">
|
||
<label>Результат:</label>
|
||
<div class="results" id="health-result"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
const API_BASE = 'http://localhost:8000';
|
||
let authToken = localStorage.getItem('auth_token') || null;
|
||
let currentUserId = localStorage.getItem('user_id') || null;
|
||
|
||
// Initialize
|
||
document.addEventListener('DOMContentLoaded', function() {
|
||
if (authToken) {
|
||
showAuthInfo();
|
||
}
|
||
});
|
||
|
||
// Tab switching
|
||
function switchTab(tabName) {
|
||
// Hide all tab contents
|
||
document.querySelectorAll('.tab-content').forEach(el => {
|
||
el.classList.remove('active');
|
||
});
|
||
document.querySelectorAll('.tab-button').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
|
||
// Show selected tab
|
||
const tabElement = document.getElementById(tabName);
|
||
if (tabElement) {
|
||
tabElement.classList.add('active');
|
||
}
|
||
|
||
// Mark button as active
|
||
event.target.classList.add('active');
|
||
}
|
||
|
||
// Auth Functions
|
||
function login() {
|
||
const username = document.getElementById('login-username').value;
|
||
const password = document.getElementById('login-password').value;
|
||
|
||
if (!username || !password) {
|
||
showStatus('login-status', 'Пожалуйста, заполните все поля', 'error');
|
||
return;
|
||
}
|
||
|
||
fetch(`${API_BASE}/api/v1/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ username, password })
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.access_token) {
|
||
authToken = data.access_token;
|
||
localStorage.setItem('auth_token', authToken);
|
||
showStatus('login-status', '✅ Успешно вошли!', 'success');
|
||
showAuthInfo();
|
||
document.getElementById('login-username').value = '';
|
||
document.getElementById('login-password').value = '';
|
||
} else {
|
||
showStatus('login-status', '❌ ' + (data.detail || 'Ошибка входа'), 'error');
|
||
}
|
||
})
|
||
.catch(e => showStatus('login-status', '❌ ' + e.message, 'error'));
|
||
}
|
||
|
||
function register() {
|
||
const email = document.getElementById('reg-email').value;
|
||
const username = document.getElementById('reg-username').value;
|
||
const password = document.getElementById('reg-password').value;
|
||
const fullName = document.getElementById('reg-fullname').value;
|
||
|
||
if (!email || !username || !password) {
|
||
showStatus('register-status', 'Пожалуйста, заполните обязательные поля', 'error');
|
||
return;
|
||
}
|
||
|
||
fetch(`${API_BASE}/api/v1/auth/register`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
email,
|
||
username,
|
||
password,
|
||
full_name: fullName
|
||
})
|
||
})
|
||
.then(r => r.json())
|
||
.then(data => {
|
||
if (data.id) {
|
||
showStatus('register-status', '✅ Успешно зарегистрированы! Теперь войдите.', 'success');
|
||
currentUserId = data.id;
|
||
localStorage.setItem('user_id', currentUserId);
|
||
document.getElementById('reg-email').value = '';
|
||
document.getElementById('reg-username').value = '';
|
||
document.getElementById('reg-password').value = '';
|
||
document.getElementById('reg-fullname').value = '';
|
||
} else {
|
||
showStatus('register-status', '❌ ' + (data.detail || 'Ошибка регистрации'), 'error');
|
||
}
|
||
})
|
||
.catch(e => showStatus('register-status', '❌ ' + e.message, 'error'));
|
||
}
|
||
|
||
function showAuthInfo() {
|
||
const tokenInfo = document.getElementById('token-info');
|
||
const tokenDisplay = document.getElementById('token-display');
|
||
if (authToken) {
|
||
tokenDisplay.textContent = authToken.substring(0, 50) + '...';
|
||
tokenInfo.style.display = 'block';
|
||
}
|
||
}
|
||
|
||
function copyToken() {
|
||
navigator.clipboard.writeText(authToken);
|
||
alert('✅ Токен скопирован в буфер обмена');
|
||
}
|
||
|
||
// API Functions
|
||
function getProfile() {
|
||
fetchAPI('/api/v1/profile', 'GET')
|
||
.then(data => showResult('users-result', data));
|
||
}
|
||
|
||
function getAllUsers() {
|
||
fetchAPI('/users', 'GET')
|
||
.then(data => showResult('users-result', data));
|
||
}
|
||
|
||
function updateProfile() {
|
||
const data = {
|
||
first_name: 'Test',
|
||
last_name: 'User',
|
||
phone: '+1234567890'
|
||
};
|
||
fetchAPI('/api/v1/profile', 'PUT', data)
|
||
.then(data => showResult('users-result', data));
|
||
}
|
||
|
||
function getAlerts() {
|
||
fetchAPI('/alerts', 'GET')
|
||
.then(data => showResult('alerts-result', data));
|
||
}
|
||
|
||
function createAlert() {
|
||
const lat = parseFloat(document.getElementById('alert-lat').value);
|
||
const lon = parseFloat(document.getElementById('alert-lon').value);
|
||
const type = document.getElementById('alert-type').value;
|
||
|
||
const data = {
|
||
user_id: currentUserId || 1,
|
||
alert_type: type,
|
||
latitude: lat,
|
||
longitude: lon,
|
||
title: 'Emergency Alert',
|
||
description: 'Test alert from API tester'
|
||
};
|
||
|
||
fetchAPI('/alerts', 'POST', data)
|
||
.then(data => showResult('alerts-result', data));
|
||
}
|
||
|
||
function getCalendarEvents() {
|
||
fetchAPI('/api/v1/events', 'GET')
|
||
.then(data => showResult('calendar-result', data));
|
||
}
|
||
|
||
function createCalendarEntry() {
|
||
const date = document.getElementById('calendar-date').value;
|
||
const type = document.getElementById('calendar-type').value;
|
||
const mood = document.getElementById('calendar-mood').value;
|
||
|
||
const data = {
|
||
entry_date: date || new Date().toISOString().split('T')[0],
|
||
entry_type: type,
|
||
mood: mood,
|
||
energy_level: 5
|
||
};
|
||
|
||
fetchAPI('/api/v1/calendar/entry', 'POST', data)
|
||
.then(data => showResult('calendar-result', data));
|
||
}
|
||
|
||
function sendNotification() {
|
||
const title = document.getElementById('notify-title').value;
|
||
const message = document.getElementById('notify-message').value;
|
||
const priority = document.getElementById('notify-priority').value;
|
||
|
||
if (!title || !message) {
|
||
showResult('notify-result', { error: 'Пожалуйста, заполните заголовок и сообщение' });
|
||
return;
|
||
}
|
||
|
||
const data = {
|
||
title: title,
|
||
body: message,
|
||
priority: priority
|
||
};
|
||
|
||
fetchAPI('/notify', 'POST', data)
|
||
.then(data => showResult('notify-result', data));
|
||
}
|
||
|
||
// Health Check Functions
|
||
function checkAllHealth() {
|
||
const services = [
|
||
{ name: 'API Gateway', url: `${API_BASE}/api/v1/health` },
|
||
{ name: 'User Service', url: 'http://localhost:8001/health' },
|
||
{ name: 'Emergency Service', url: 'http://localhost:8002/health' },
|
||
{ name: 'Calendar Service', url: 'http://localhost:8004/health' },
|
||
{ name: 'Notification Service', url: 'http://localhost:8005/health' }
|
||
];
|
||
|
||
let results = [];
|
||
Promise.all(services.map(service =>
|
||
fetch(service.url)
|
||
.then(r => r.json())
|
||
.then(data => ({
|
||
name: service.name,
|
||
status: data.status || '❌ Unknown',
|
||
ok: data.status === 'healthy'
|
||
}))
|
||
.catch(e => ({
|
||
name: service.name,
|
||
status: '❌ Offline',
|
||
ok: false
|
||
}))
|
||
)).then(healthData => {
|
||
showResult('health-result', {
|
||
services: healthData,
|
||
timestamp: new Date().toLocaleString('ru-RU')
|
||
});
|
||
});
|
||
}
|
||
|
||
function checkGatewayHealth() {
|
||
fetchAPI('/api/v1/health', 'GET')
|
||
.then(data => showResult('health-result', data));
|
||
}
|
||
|
||
function checkUserServiceHealth() {
|
||
fetch('http://localhost:8001/health')
|
||
.then(r => r.json())
|
||
.then(data => showResult('health-result', data))
|
||
.catch(e => showResult('health-result', { error: e.message }));
|
||
}
|
||
|
||
function checkEmergencyServiceHealth() {
|
||
fetch('http://localhost:8002/health')
|
||
.then(r => r.json())
|
||
.then(data => showResult('health-result', data))
|
||
.catch(e => showResult('health-result', { error: e.message }));
|
||
}
|
||
|
||
function checkCalendarServiceHealth() {
|
||
fetch('http://localhost:8004/health')
|
||
.then(r => r.json())
|
||
.then(data => showResult('health-result', data))
|
||
.catch(e => showResult('health-result', { error: e.message }));
|
||
}
|
||
|
||
function checkNotificationServiceHealth() {
|
||
fetch('http://localhost:8005/health')
|
||
.then(r => r.json())
|
||
.then(data => showResult('health-result', data))
|
||
.catch(e => showResult('health-result', { error: e.message }));
|
||
}
|
||
|
||
// Utility Functions
|
||
function fetchAPI(endpoint, method = 'GET', data = null) {
|
||
const options = {
|
||
method: method,
|
||
headers: { 'Content-Type': 'application/json' }
|
||
};
|
||
|
||
if (authToken) {
|
||
options.headers['Authorization'] = `Bearer ${authToken}`;
|
||
}
|
||
|
||
if (data) {
|
||
options.body = JSON.stringify(data);
|
||
}
|
||
|
||
return fetch(API_BASE + endpoint, options)
|
||
.then(r => r.json())
|
||
.catch(e => ({ error: e.message }));
|
||
}
|
||
|
||
function showStatus(elementId, message, type) {
|
||
const el = document.getElementById(elementId);
|
||
el.className = `status ${type}`;
|
||
el.textContent = message;
|
||
}
|
||
|
||
function showResult(elementId, data) {
|
||
const el = document.getElementById(elementId);
|
||
el.innerHTML = '<pre>' + JSON.stringify(data, null, 2) + '</pre>';
|
||
}
|
||
|
||
// ===== WebSocket SOS Functions =====
|
||
let wsConnection = null;
|
||
let wsState = {
|
||
connected: false,
|
||
pingCount: 0,
|
||
pongCount: 0,
|
||
messageCount: 0,
|
||
lastPingTime: null
|
||
};
|
||
|
||
function wsConnect() {
|
||
if (!authToken) {
|
||
alert('Сначала необходимо авторизоваться!');
|
||
return;
|
||
}
|
||
|
||
const wsUrl = `ws://localhost:8002/api/v1/emergency/ws/${currentUserId}?token=${authToken}`;
|
||
|
||
try {
|
||
wsConnection = new WebSocket(wsUrl);
|
||
|
||
wsConnection.onopen = () => {
|
||
wsState.connected = true;
|
||
updateWSIndicator();
|
||
wsAddLog('✅ WebSocket подключен!', 'success');
|
||
document.getElementById('ws-status').textContent = '🟢 Подключено';
|
||
};
|
||
|
||
wsConnection.onmessage = (event) => {
|
||
wsState.messageCount++;
|
||
const data = JSON.parse(event.data);
|
||
|
||
if (data.type === 'pong') {
|
||
wsState.pongCount++;
|
||
const latency = Date.now() - wsState.lastPingTime;
|
||
document.getElementById('ws-pong-count').textContent = wsState.pongCount;
|
||
document.getElementById('ws-latency').textContent = latency;
|
||
wsAddLog(`🔄 Pong получен (${latency}ms)`, 'success');
|
||
} else if (data.type === 'emergency_alert') {
|
||
wsAddLog(`🚨 Alert received: ${JSON.stringify(data)}`, 'error');
|
||
} else {
|
||
wsAddLog(`📨 Message: ${JSON.stringify(data)}`, 'info');
|
||
}
|
||
|
||
document.getElementById('ws-msg-count').textContent = wsState.messageCount;
|
||
};
|
||
|
||
wsConnection.onerror = (error) => {
|
||
wsAddLog(`❌ Error: ${error}`, 'error');
|
||
};
|
||
|
||
wsConnection.onclose = () => {
|
||
wsState.connected = false;
|
||
updateWSIndicator();
|
||
wsAddLog('❌ WebSocket отключен', 'warning');
|
||
document.getElementById('ws-status').textContent = '🔴 Отключено';
|
||
};
|
||
} catch (error) {
|
||
wsAddLog(`❌ Failed to connect: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function wsDisconnect() {
|
||
if (wsConnection) {
|
||
wsConnection.close();
|
||
wsState.connected = false;
|
||
updateWSIndicator();
|
||
}
|
||
}
|
||
|
||
function wsSendPing() {
|
||
if (!wsConnection || !wsState.connected) {
|
||
alert('WebSocket не подключен!');
|
||
return;
|
||
}
|
||
|
||
wsState.lastPingTime = Date.now();
|
||
wsState.pingCount++;
|
||
wsConnection.send(JSON.stringify({ type: 'ping' }));
|
||
document.getElementById('ws-ping-count').textContent = wsState.pingCount;
|
||
wsAddLog('📤 Ping отправлен', 'info');
|
||
}
|
||
|
||
async function wsCreateSOS() {
|
||
if (!authToken) {
|
||
alert('Сначала необходимо авторизоваться!');
|
||
return;
|
||
}
|
||
|
||
const lat = document.getElementById('ws-lat').value;
|
||
const lon = document.getElementById('ws-lon').value;
|
||
const address = document.getElementById('ws-address').value;
|
||
const alertType = document.getElementById('ws-alert-type').value;
|
||
const message = document.getElementById('ws-message').value;
|
||
|
||
if (!lat || !lon || !address || !message) {
|
||
alert('Заполните все поля!');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const response = await fetch('http://localhost:8002/api/v1/alert', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${authToken}`
|
||
},
|
||
body: JSON.stringify({
|
||
latitude: parseFloat(lat),
|
||
longitude: parseFloat(lon),
|
||
address: address,
|
||
alert_type: alertType,
|
||
message: message,
|
||
contact_emergency_services: true,
|
||
notify_emergency_contacts: true
|
||
})
|
||
});
|
||
|
||
if (response.ok) {
|
||
const alert = await response.json();
|
||
wsAddLog(`🚨 SOS Alert создан! ID: ${alert.id}`, 'success');
|
||
} else {
|
||
wsAddLog(`❌ Ошибка: ${response.status}`, 'error');
|
||
}
|
||
} catch (error) {
|
||
wsAddLog(`❌ ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function wsAddLog(message, type) {
|
||
const log = document.getElementById('ws-log');
|
||
const entry = document.createElement('div');
|
||
entry.className = `log-entry ${type}`;
|
||
const timestamp = new Date().toLocaleTimeString('ru-RU');
|
||
entry.innerHTML = `[${timestamp}] ${message}`;
|
||
log.appendChild(entry);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
function clearWSLog() {
|
||
document.getElementById('ws-log').innerHTML = '';
|
||
}
|
||
|
||
function updateWSIndicator() {
|
||
const indicator = document.getElementById('ws-indicator');
|
||
if (wsState.connected) {
|
||
indicator.classList.remove('disconnected');
|
||
indicator.classList.add('connected');
|
||
} else {
|
||
indicator.classList.add('disconnected');
|
||
indicator.classList.remove('connected');
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|