Files
chat/frontend_test.html
Andrew K. Choi cfc93cb99a
All checks were successful
continuous-integration/drone/push Build is passing
feat: Fix nutrition service and add location-based alerts
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
2025-12-13 16:34:50 +09:00

1144 lines
43 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>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>