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
1207 lines
42 KiB
HTML
1207 lines
42 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>🚨 WebSocket SOS Service 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, #1e3c72 0%, #2a5298 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
header {
|
||
text-align: center;
|
||
color: white;
|
||
margin-bottom: 30px;
|
||
padding: 20px;
|
||
background: rgba(0,0,0,0.2);
|
||
border-radius: 10px;
|
||
border-left: 4px solid #ff4444;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 2.5em;
|
||
margin-bottom: 10px;
|
||
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
||
}
|
||
|
||
header p {
|
||
font-size: 1em;
|
||
opacity: 0.95;
|
||
}
|
||
|
||
.tabs {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 12px 20px;
|
||
border: none;
|
||
background: rgba(255,255,255,0.2);
|
||
color: white;
|
||
cursor: pointer;
|
||
border-radius: 5px;
|
||
font-weight: bold;
|
||
transition: all 0.3s;
|
||
}
|
||
|
||
.tab-btn:hover {
|
||
background: rgba(255,255,255,0.3);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.tab-btn.active {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.grid-2 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.grid-3 {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.panel {
|
||
background: white;
|
||
border-radius: 10px;
|
||
padding: 20px;
|
||
box-shadow: 0 5px 20px rgba(0,0,0,0.1);
|
||
border-left: 4px solid #667eea;
|
||
}
|
||
|
||
.panel.warning {
|
||
border-left-color: #ff9800;
|
||
}
|
||
|
||
.panel.success {
|
||
border-left-color: #4caf50;
|
||
}
|
||
|
||
.panel.danger {
|
||
border-left-color: #f44336;
|
||
}
|
||
|
||
.panel h2 {
|
||
color: #667eea;
|
||
margin-bottom: 15px;
|
||
font-size: 1.5em;
|
||
}
|
||
|
||
.panel h3 {
|
||
color: #555;
|
||
margin: 15px 0 10px 0;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
label {
|
||
display: block;
|
||
margin-bottom: 5px;
|
||
color: #555;
|
||
font-weight: bold;
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
input[type="text"],
|
||
input[type="email"],
|
||
input[type="password"],
|
||
input[type="number"],
|
||
textarea,
|
||
select {
|
||
width: 100%;
|
||
padding: 10px;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 5px;
|
||
font-size: 0.95em;
|
||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||
transition: border-color 0.3s;
|
||
}
|
||
|
||
input[type="text"]:focus,
|
||
input[type="email"]:focus,
|
||
input[type="password"]:focus,
|
||
input[type="number"]:focus,
|
||
textarea:focus,
|
||
select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 10px rgba(102, 126, 234, 0.2);
|
||
}
|
||
|
||
textarea {
|
||
resize: vertical;
|
||
min-height: 80px;
|
||
}
|
||
|
||
.button-group {
|
||
display: flex;
|
||
gap: 10px;
|
||
flex-wrap: wrap;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
button {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 5px;
|
||
font-weight: bold;
|
||
cursor: pointer;
|
||
transition: all 0.3s;
|
||
font-size: 0.95em;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
.btn-success {
|
||
background: #4caf50;
|
||
color: white;
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: #45a049;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #da190b;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.btn-warning {
|
||
background: #ff9800;
|
||
color: white;
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: #e68900;
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.status {
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
font-weight: bold;
|
||
display: none;
|
||
animation: slideIn 0.3s ease;
|
||
}
|
||
|
||
.status.show {
|
||
display: block;
|
||
}
|
||
|
||
.status.success {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
border: 2px solid #4caf50;
|
||
}
|
||
|
||
.status.error {
|
||
background: #f8d7da;
|
||
color: #721c24;
|
||
border: 2px solid #f44336;
|
||
}
|
||
|
||
.status.info {
|
||
background: #d1ecf1;
|
||
color: #0c5460;
|
||
border: 2px solid #667eea;
|
||
}
|
||
|
||
.status.warning {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
border: 2px solid #ff9800;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-10px);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
.message-log {
|
||
background: #f5f5f5;
|
||
border: 2px solid #e0e0e0;
|
||
border-radius: 5px;
|
||
padding: 15px;
|
||
height: 300px;
|
||
overflow-y: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.log-entry {
|
||
padding: 8px;
|
||
margin: 5px 0;
|
||
border-radius: 3px;
|
||
word-break: break-all;
|
||
animation: slideIn 0.2s ease;
|
||
}
|
||
|
||
.log-entry.info {
|
||
background: #e3f2fd;
|
||
color: #1565c0;
|
||
border-left: 3px solid #667eea;
|
||
}
|
||
|
||
.log-entry.success {
|
||
background: #e8f5e9;
|
||
color: #2e7d32;
|
||
border-left: 3px solid #4caf50;
|
||
}
|
||
|
||
.log-entry.error {
|
||
background: #ffebee;
|
||
color: #c62828;
|
||
border-left: 3px solid #f44336;
|
||
}
|
||
|
||
.log-entry.warning {
|
||
background: #fff8e1;
|
||
color: #f57f17;
|
||
border-left: 3px solid #ff9800;
|
||
}
|
||
|
||
.log-entry.timestamp {
|
||
font-size: 0.85em;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.stats {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
margin: 20px 0;
|
||
}
|
||
|
||
.stat-card {
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
color: white;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
|
||
}
|
||
|
||
.stat-value {
|
||
font-size: 2em;
|
||
font-weight: bold;
|
||
margin-bottom: 5px;
|
||
}
|
||
|
||
.stat-label {
|
||
font-size: 0.9em;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
.connection-indicator {
|
||
display: inline-block;
|
||
width: 15px;
|
||
height: 15px;
|
||
border-radius: 50%;
|
||
margin-right: 10px;
|
||
animation: pulse 2s infinite;
|
||
}
|
||
|
||
.connection-indicator.connected {
|
||
background: #4caf50;
|
||
}
|
||
|
||
.connection-indicator.disconnected {
|
||
background: #f44336;
|
||
animation: none;
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% {
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
opacity: 0.5;
|
||
}
|
||
}
|
||
|
||
.alert-box {
|
||
padding: 15px;
|
||
border-radius: 5px;
|
||
margin: 15px 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.alert-box.info {
|
||
background: #e3f2fd;
|
||
border-left: 4px solid #667eea;
|
||
color: #1565c0;
|
||
}
|
||
|
||
.alert-box.success {
|
||
background: #e8f5e9;
|
||
border-left: 4px solid #4caf50;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.alert-box.warning {
|
||
background: #fff3cd;
|
||
border-left: 4px solid #ff9800;
|
||
color: #856404;
|
||
}
|
||
|
||
.alert-box.error {
|
||
background: #f8d7da;
|
||
border-left: 4px solid #f44336;
|
||
color: #721c24;
|
||
}
|
||
|
||
.code-block {
|
||
background: #f5f5f5;
|
||
border: 1px solid #ddd;
|
||
border-radius: 5px;
|
||
padding: 15px;
|
||
overflow-x: auto;
|
||
font-family: 'Courier New', monospace;
|
||
font-size: 0.9em;
|
||
margin: 10px 0;
|
||
}
|
||
|
||
.highlight {
|
||
background: #fffacd;
|
||
padding: 2px 4px;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
footer {
|
||
text-align: center;
|
||
color: white;
|
||
margin-top: 40px;
|
||
padding: 20px;
|
||
opacity: 0.8;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.grid-2, .grid-3 {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
header h1 {
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
.tabs {
|
||
flex-direction: column;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<header>
|
||
<h1>🚨 WebSocket SOS Service Tester</h1>
|
||
<p>Real-time Emergency Signal Testing Platform</p>
|
||
</header>
|
||
|
||
<div class="tabs">
|
||
<button class="tab-btn active" onclick="switchTab('auth')">🔐 Authentication</button>
|
||
<button class="tab-btn" onclick="switchTab('websocket')">📡 WebSocket</button>
|
||
<button class="tab-btn" onclick="switchTab('sos')">🚨 SOS Alert</button>
|
||
<button class="tab-btn" onclick="switchTab('monitor')">📊 Monitor</button>
|
||
<button class="tab-btn" onclick="switchTab('guide')">📖 Guide</button>
|
||
</div>
|
||
|
||
<!-- 🔐 Authentication Tab -->
|
||
<div id="auth" class="tab-content active">
|
||
<div class="grid-2">
|
||
<div class="panel">
|
||
<h2>🔐 Login</h2>
|
||
<div class="form-group">
|
||
<label>Email:</label>
|
||
<input type="email" id="login-email" placeholder="wstester@test.com" value="wstester@test.com">
|
||
</div>
|
||
<div class="form-group">
|
||
<label>Password:</label>
|
||
<input type="password" id="login-password" placeholder="WsTest1234!" value="WsTest1234!">
|
||
</div>
|
||
<div class="button-group">
|
||
<button class="btn-primary" onclick="login()">🔓 Login</button>
|
||
<button class="btn-warning" onclick="clearAuth()">🗑️ Clear</button>
|
||
</div>
|
||
<div id="auth-status" class="status"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>📋 Token Info</h2>
|
||
<div id="token-info" style="font-family: monospace; font-size: 0.9em; word-break: break-all;">
|
||
<p style="color: #999;">No token yet. Login first.</p>
|
||
</div>
|
||
<button class="btn-warning" onclick="copyToken()" style="margin-top: 15px;">📋 Copy Token</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 📡 WebSocket Tab -->
|
||
<div id="websocket" class="tab-content">
|
||
<div class="grid-2">
|
||
<div class="panel">
|
||
<h2>📡 WebSocket Connection</h2>
|
||
<p style="margin-bottom: 15px;">
|
||
<span class="connection-indicator disconnected" id="ws-indicator"></span>
|
||
<span id="ws-status">Disconnected</span>
|
||
</p>
|
||
|
||
<div class="form-group">
|
||
<label>WebSocket URL:</label>
|
||
<input type="text" id="ws-url" placeholder="ws://localhost:8002/api/v1/emergency/ws/{user_id}?token={jwt_token}" readonly>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button class="btn-success" onclick="connectWebSocket()">🔌 Connect</button>
|
||
<button class="btn-danger" onclick="disconnectWebSocket()">❌ Disconnect</button>
|
||
</div>
|
||
|
||
<div id="ws-status-msg" class="status"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>🏓 Ping/Pong Test</h2>
|
||
<p>Test connection heartbeat:</p>
|
||
<div class="stats">
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ping-count">0</div>
|
||
<div class="stat-label">Pings Sent</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="pong-count">0</div>
|
||
<div class="stat-label">Pongs Received</div>
|
||
</div>
|
||
<div class="stat-card">
|
||
<div class="stat-value" id="ping-latency">--</div>
|
||
<div class="stat-label">Latency (ms)</div>
|
||
</div>
|
||
</div>
|
||
<button class="btn-primary" onclick="sendPing()">📤 Send Ping</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>📨 WebSocket Messages Log</h2>
|
||
<button class="btn-warning" onclick="clearLog()">🗑️ Clear Log</button>
|
||
<div id="ws-log" class="message-log"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 🚨 SOS Alert Tab -->
|
||
<div id="sos" class="tab-content">
|
||
<div class="grid-2">
|
||
<div class="panel">
|
||
<h2>🚨 Create SOS Alert</h2>
|
||
|
||
<div class="form-group">
|
||
<label>Alert Type:</label>
|
||
<select id="alert-type">
|
||
<option value="violence">🚨 Violence</option>
|
||
<option value="medical">🏥 Medical</option>
|
||
<option value="harassment">😠 Harassment</option>
|
||
<option value="unsafe_area">⚠️ Unsafe Area</option>
|
||
<option value="accident">🚗 Accident</option>
|
||
<option value="fire">🔥 Fire</option>
|
||
<option value="general">📍 General Emergency</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Latitude:</label>
|
||
<input type="number" id="alert-lat" placeholder="55.7558" value="55.7558" step="0.0001">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Longitude:</label>
|
||
<input type="number" id="alert-lon" placeholder="37.6173" value="37.6173" step="0.0001">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Address:</label>
|
||
<input type="text" id="alert-address" placeholder="Current location" value="Test Location">
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Message:</label>
|
||
<textarea id="alert-message" placeholder="Describe the emergency...">🚨 WebSocket Test SOS Alert</textarea>
|
||
</div>
|
||
|
||
<div class="button-group">
|
||
<button class="btn-danger" onclick="createSOSAlert()">🚨 Create SOS Alert</button>
|
||
<button class="btn-warning" onclick="clearSOSForm()">🗑️ Clear</button>
|
||
</div>
|
||
|
||
<div id="sos-status" class="status"></div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>📍 Location Info</h2>
|
||
<button class="btn-primary" onclick="getLocation()" style="margin-bottom: 15px;">📍 Get Current Location</button>
|
||
<div id="location-info" style="background: #f5f5f5; padding: 15px; border-radius: 5px; min-height: 100px;">
|
||
<p style="color: #999;">Location will be shown here</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>📊 Recent Alerts</h2>
|
||
<div id="alerts-list" style="max-height: 300px; overflow-y: auto;">
|
||
<p style="color: #999;">No alerts created yet</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 📊 Monitor Tab -->
|
||
<div id="monitor" class="tab-content">
|
||
<div class="grid-3">
|
||
<div class="panel">
|
||
<h3>🟢 Connection Status</h3>
|
||
<div class="stat-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin-top: 10px;">
|
||
<div id="conn-status" class="stat-value">OFF</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h3>📨 Messages Received</h3>
|
||
<div class="stat-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin-top: 10px;">
|
||
<div id="msg-count" class="stat-value">0</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h3>⏱️ Uptime</h3>
|
||
<div class="stat-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); margin-top: 10px;">
|
||
<div id="uptime" class="stat-value">0s</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid-2">
|
||
<div class="panel">
|
||
<h3>📈 Performance Metrics</h3>
|
||
<table style="width: 100%; border-collapse: collapse;">
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 8px;">Avg Latency:</td>
|
||
<td style="text-align: right; font-weight: bold;" id="avg-latency">--</td>
|
||
</tr>
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 8px;">Max Latency:</td>
|
||
<td style="text-align: right; font-weight: bold;" id="max-latency">--</td>
|
||
</tr>
|
||
<tr style="border-bottom: 1px solid #eee;">
|
||
<td style="padding: 8px;">Min Latency:</td>
|
||
<td style="text-align: right; font-weight: bold;" id="min-latency">--</td>
|
||
</tr>
|
||
<tr>
|
||
<td style="padding: 8px;">Success Rate:</td>
|
||
<td style="text-align: right; font-weight: bold;" id="success-rate">--</td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h3>🧪 Auto Test</h3>
|
||
<p style="margin-bottom: 15px; font-size: 0.95em;">Run automated tests every 30 seconds:</p>
|
||
<div class="button-group">
|
||
<button class="btn-primary" onclick="startAutoTest()">▶️ Start</button>
|
||
<button class="btn-danger" onclick="stopAutoTest()">⏹️ Stop</button>
|
||
</div>
|
||
<div id="auto-test-status" class="status" style="display: none;"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="panel">
|
||
<h2>📊 System Statistics</h2>
|
||
<div id="system-stats" style="font-family: monospace; font-size: 0.9em; white-space: pre-wrap; word-break: break-word; max-height: 200px; overflow-y: auto; background: #f5f5f5; padding: 15px; border-radius: 5px;">
|
||
Waiting for data...
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 📖 Guide Tab -->
|
||
<div id="guide" class="tab-content">
|
||
<div class="panel">
|
||
<h2>📖 How to Test WebSocket SOS Service</h2>
|
||
|
||
<h3>1. 🔐 Step 1: Authenticate</h3>
|
||
<ol style="margin: 10px 0 20px 20px;">
|
||
<li>Go to <strong>Authentication</strong> tab</li>
|
||
<li>Enter credentials (default: wstester@test.com / WsTest1234!)</li>
|
||
<li>Click <strong>Login</strong></li>
|
||
<li>You should see a JWT token in the Token Info panel</li>
|
||
</ol>
|
||
|
||
<h3>2. 📡 Step 2: Connect WebSocket</h3>
|
||
<ol style="margin: 10px 0 20px 20px;">
|
||
<li>Go to <strong>WebSocket</strong> tab</li>
|
||
<li>Click <strong>Connect</strong> button</li>
|
||
<li>You should see connection status change to "Connected"</li>
|
||
<li>Green indicator means WebSocket is active</li>
|
||
</ol>
|
||
|
||
<h3>3. 🏓 Step 3: Test Ping/Pong</h3>
|
||
<ol style="margin: 10px 0 20px 20px;">
|
||
<li>Click <strong>Send Ping</strong> button</li>
|
||
<li>Watch for pong response in the message log</li>
|
||
<li>Latency should be < 100ms</li>
|
||
</ol>
|
||
|
||
<h3>4. 🚨 Step 4: Create SOS Alert</h3>
|
||
<ol style="margin: 10px 0 20px 20px;">
|
||
<li>Go to <strong>SOS Alert</strong> tab</li>
|
||
<li>Fill in location (latitude/longitude)</li>
|
||
<li>Enter message</li>
|
||
<li>Click <strong>Create SOS Alert</strong></li>
|
||
<li>Check WebSocket Messages Log for alert notification</li>
|
||
</ol>
|
||
|
||
<h3>5. 📊 Step 5: Monitor Results</h3>
|
||
<ol style="margin: 10px 0 20px 20px;">
|
||
<li>Go to <strong>Monitor</strong> tab</li>
|
||
<li>View connection status, message count, and uptime</li>
|
||
<li>Check performance metrics (latency, success rate)</li>
|
||
</ol>
|
||
|
||
<div class="alert-box info" style="margin-top: 20px;">
|
||
<span style="font-size: 1.2em;">ℹ️</span>
|
||
<div>
|
||
<strong>Default Test Credentials:</strong><br>
|
||
Email: <code style="background: #f5f5f5; padding: 2px 5px;">wstester@test.com</code><br>
|
||
Password: <code style="background: #f5f5f5; padding: 2px 5px;">WsTest1234!</code>
|
||
</div>
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px;">🔧 API Endpoints</h3>
|
||
<div class="code-block">
|
||
POST /api/v1/auth/login
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"email": "wstester@test.com",
|
||
"password": "WsTest1234!"
|
||
}
|
||
</div>
|
||
|
||
<div class="code-block">
|
||
WebSocket: ws://localhost:8002/api/v1/emergency/ws/{user_id}?token={jwt_token}
|
||
</div>
|
||
|
||
<div class="code-block">
|
||
POST /api/v1/alert
|
||
Authorization: Bearer {jwt_token}
|
||
Content-Type: application/json
|
||
|
||
{
|
||
"latitude": 55.7558,
|
||
"longitude": 37.6173,
|
||
"address": "Location",
|
||
"alert_type": "SOS",
|
||
"message": "Emergency message"
|
||
}
|
||
</div>
|
||
|
||
<h3 style="margin-top: 30px;">💡 Tips</h3>
|
||
<ul style="margin: 10px 0 20px 20px;">
|
||
<li>Keep WebSocket connected while testing SOS alerts</li>
|
||
<li>Messages appear in real-time in the WebSocket Messages Log</li>
|
||
<li>Green indicator = Connection is active</li>
|
||
<li>Red indicator = Connection is lost</li>
|
||
<li>Latency < 100ms is excellent</li>
|
||
<li>Use Monitor tab to track performance</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<footer>
|
||
<p>🚨 WebSocket SOS Service Tester | v1.0 | Test Date: 13 Dec 2025</p>
|
||
</footer>
|
||
|
||
<script>
|
||
// Global state
|
||
let state = {
|
||
token: localStorage.getItem('jwt_token') || null,
|
||
userId: localStorage.getItem('user_id') || null,
|
||
ws: null,
|
||
connected: false,
|
||
pingCount: 0,
|
||
pongCount: 0,
|
||
messageCount: 0,
|
||
messages: [],
|
||
alerts: [],
|
||
startTime: null,
|
||
latencies: [],
|
||
autoTestRunning: false,
|
||
autoTestInterval: null
|
||
};
|
||
|
||
// API Configuration
|
||
const CONFIG = {
|
||
userServiceUrl: 'http://localhost:8001',
|
||
emergencyServiceUrl: 'http://localhost:8002',
|
||
wsUrl: 'ws://localhost:8002'
|
||
};
|
||
|
||
// Tab switching
|
||
function switchTab(tabName) {
|
||
document.querySelectorAll('.tab-content').forEach(tab => {
|
||
tab.classList.remove('active');
|
||
});
|
||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||
btn.classList.remove('active');
|
||
});
|
||
|
||
document.getElementById(tabName).classList.add('active');
|
||
event.target.classList.add('active');
|
||
}
|
||
|
||
// ===== Authentication Functions =====
|
||
async function login() {
|
||
const email = document.getElementById('login-email').value;
|
||
const password = document.getElementById('login-password').value;
|
||
|
||
if (!email || !password) {
|
||
showStatus('auth-status', 'Please fill in all fields', 'error');
|
||
return;
|
||
}
|
||
|
||
showStatus('auth-status', 'Logging in...', 'info');
|
||
|
||
try {
|
||
const response = await fetch(`${CONFIG.userServiceUrl}/api/v1/auth/login`, {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ email, password })
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Login failed: ${response.status}`);
|
||
}
|
||
|
||
const data = await response.json();
|
||
state.token = data.access_token;
|
||
|
||
// Decode JWT to get user_id
|
||
const parts = state.token.split('.');
|
||
if (parts.length === 3) {
|
||
const payload = JSON.parse(atob(parts[1]));
|
||
state.userId = payload.sub;
|
||
}
|
||
|
||
localStorage.setItem('jwt_token', state.token);
|
||
localStorage.setItem('user_id', state.userId);
|
||
|
||
showStatus('auth-status', '✅ Login successful!', 'success');
|
||
displayTokenInfo();
|
||
updateWSUrl();
|
||
} catch (error) {
|
||
showStatus('auth-status', `❌ ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function displayTokenInfo() {
|
||
const info = document.getElementById('token-info');
|
||
if (state.token) {
|
||
info.innerHTML = `
|
||
<p><strong>Token:</strong></p>
|
||
<p style="word-break: break-all; background: #f5f5f5; padding: 10px; border-radius: 5px; font-size: 0.85em;">${state.token.substring(0, 100)}...</p>
|
||
<p><strong>User ID:</strong> ${state.userId}</p>
|
||
<p><strong>Status:</strong> <span style="color: #4caf50;">✅ Authenticated</span></p>
|
||
`;
|
||
}
|
||
}
|
||
|
||
function copyToken() {
|
||
if (state.token) {
|
||
navigator.clipboard.writeText(state.token);
|
||
showStatus('auth-status', 'Token copied to clipboard!', 'success');
|
||
}
|
||
}
|
||
|
||
function clearAuth() {
|
||
state.token = null;
|
||
state.userId = null;
|
||
localStorage.removeItem('jwt_token');
|
||
localStorage.removeItem('user_id');
|
||
document.getElementById('token-info').innerHTML = '<p style="color: #999;">No token yet. Login first.</p>';
|
||
showStatus('auth-status', 'Authentication cleared', 'info');
|
||
}
|
||
|
||
// ===== WebSocket Functions =====
|
||
function updateWSUrl() {
|
||
if (state.token && state.userId) {
|
||
const url = `${CONFIG.wsUrl}/api/v1/emergency/ws/${state.userId}?token=${state.token}`;
|
||
document.getElementById('ws-url').value = url;
|
||
}
|
||
}
|
||
|
||
function connectWebSocket() {
|
||
if (!state.token) {
|
||
showStatus('ws-status-msg', 'Please login first', 'error');
|
||
return;
|
||
}
|
||
|
||
if (state.ws && state.connected) {
|
||
showStatus('ws-status-msg', 'Already connected', 'warning');
|
||
return;
|
||
}
|
||
|
||
const url = `${CONFIG.wsUrl}/api/v1/emergency/ws/${state.userId}?token=${state.token}`;
|
||
|
||
try {
|
||
state.ws = new WebSocket(url);
|
||
|
||
state.ws.onopen = () => {
|
||
state.connected = true;
|
||
state.startTime = new Date();
|
||
updateConnectionIndicator();
|
||
showStatus('ws-status-msg', '✅ WebSocket connected!', 'success');
|
||
addLog('Connection established', 'success');
|
||
};
|
||
|
||
state.ws.onmessage = (event) => {
|
||
state.messageCount++;
|
||
addLog(`Message: ${event.data}`, 'info');
|
||
|
||
try {
|
||
const data = JSON.parse(event.data);
|
||
if (data.type === 'pong') {
|
||
state.pongCount++;
|
||
const latency = Date.now() - (state.lastPingTime || 0);
|
||
state.latencies.push(latency);
|
||
document.getElementById('pong-count').textContent = state.pongCount;
|
||
document.getElementById('ping-latency').textContent = latency + 'ms';
|
||
} else if (data.type === 'emergency_alert') {
|
||
addLog(`🚨 SOS Alert: ${JSON.stringify(data)}`, 'error');
|
||
}
|
||
} catch (e) {
|
||
// Invalid JSON, just log as-is
|
||
}
|
||
|
||
document.getElementById('msg-count').textContent = state.messageCount;
|
||
};
|
||
|
||
state.ws.onerror = (error) => {
|
||
addLog(`Error: ${error.message}`, 'error');
|
||
showStatus('ws-status-msg', `❌ WebSocket error: ${error}`, 'error');
|
||
};
|
||
|
||
state.ws.onclose = () => {
|
||
state.connected = false;
|
||
updateConnectionIndicator();
|
||
showStatus('ws-status-msg', '⚠️ WebSocket disconnected', 'warning');
|
||
addLog('Disconnected', 'warning');
|
||
};
|
||
} catch (error) {
|
||
showStatus('ws-status-msg', `❌ Failed to connect: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function disconnectWebSocket() {
|
||
if (state.ws) {
|
||
state.ws.close();
|
||
state.connected = false;
|
||
updateConnectionIndicator();
|
||
showStatus('ws-status-msg', 'Disconnected', 'info');
|
||
}
|
||
}
|
||
|
||
function sendPing() {
|
||
if (!state.connected) {
|
||
showStatus('ws-status-msg', 'WebSocket not connected', 'error');
|
||
return;
|
||
}
|
||
|
||
state.lastPingTime = Date.now();
|
||
state.pingCount++;
|
||
state.ws.send(JSON.stringify({ type: 'ping' }));
|
||
document.getElementById('ping-count').textContent = state.pingCount;
|
||
addLog('Ping sent', 'info');
|
||
}
|
||
|
||
// ===== SOS Alert Functions =====
|
||
async function createSOSAlert() {
|
||
if (!state.token) {
|
||
showStatus('sos-status', 'Please login first', 'error');
|
||
return;
|
||
}
|
||
|
||
const alertType = document.getElementById('alert-type').value;
|
||
const lat = parseFloat(document.getElementById('alert-lat').value);
|
||
const lon = parseFloat(document.getElementById('alert-lon').value);
|
||
const address = document.getElementById('alert-address').value;
|
||
const message = document.getElementById('alert-message').value;
|
||
|
||
if (!lat || !lon || !address || !message) {
|
||
showStatus('sos-status', 'Please fill in all fields', 'error');
|
||
return;
|
||
}
|
||
|
||
showStatus('sos-status', 'Creating SOS alert...', 'info');
|
||
|
||
try {
|
||
const response = await fetch(`${CONFIG.emergencyServiceUrl}/api/v1/alert`, {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Bearer ${state.token}`
|
||
},
|
||
body: JSON.stringify({
|
||
latitude: lat,
|
||
longitude: lon,
|
||
address: address,
|
||
alert_type: alertType,
|
||
message: message,
|
||
contact_emergency_services: true,
|
||
notify_emergency_contacts: true
|
||
})
|
||
});
|
||
|
||
if (!response.ok) {
|
||
throw new Error(`Failed: ${response.status}`);
|
||
}
|
||
|
||
const alert = await response.json();
|
||
state.alerts.unshift(alert);
|
||
|
||
showStatus('sos-status', '✅ SOS Alert created!', 'success');
|
||
addLog(`🚨 SOS Alert created: ID=${alert.id}`, 'success');
|
||
updateAlertsList();
|
||
clearSOSForm();
|
||
} catch (error) {
|
||
showStatus('sos-status', `❌ ${error.message}`, 'error');
|
||
addLog(`Error creating alert: ${error.message}`, 'error');
|
||
}
|
||
}
|
||
|
||
function getLocation() {
|
||
if (navigator.geolocation) {
|
||
navigator.geolocation.getCurrentPosition((position) => {
|
||
const lat = position.coords.latitude;
|
||
const lon = position.coords.longitude;
|
||
|
||
document.getElementById('alert-lat').value = lat.toFixed(4);
|
||
document.getElementById('alert-lon').value = lon.toFixed(4);
|
||
|
||
const info = document.getElementById('location-info');
|
||
info.innerHTML = `
|
||
<p><strong>📍 Current Location:</strong></p>
|
||
<p>Latitude: <strong>${lat.toFixed(6)}</strong></p>
|
||
<p>Longitude: <strong>${lon.toFixed(6)}</strong></p>
|
||
<p>Accuracy: <strong>${position.coords.accuracy.toFixed(0)}m</strong></p>
|
||
`;
|
||
}, () => {
|
||
document.getElementById('location-info').innerHTML = '<p style="color: #f44336;">❌ Location access denied</p>';
|
||
});
|
||
} else {
|
||
document.getElementById('location-info').innerHTML = '<p style="color: #f44336;">❌ Geolocation not supported</p>';
|
||
}
|
||
}
|
||
|
||
function updateAlertsList() {
|
||
const list = document.getElementById('alerts-list');
|
||
if (state.alerts.length === 0) {
|
||
list.innerHTML = '<p style="color: #999;">No alerts created yet</p>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = state.alerts.map(alert => `
|
||
<div style="background: #f5f5f5; padding: 10px; margin: 10px 0; border-radius: 5px; border-left: 4px solid #f44336;">
|
||
<p><strong>ID:</strong> ${alert.id}</p>
|
||
<p><strong>Type:</strong> ${alert.alert_type}</p>
|
||
<p><strong>Location:</strong> ${alert.address}</p>
|
||
<p><strong>Message:</strong> ${alert.message}</p>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function clearSOSForm() {
|
||
document.getElementById('alert-type').value = 'SOS';
|
||
document.getElementById('alert-lat').value = '55.7558';
|
||
document.getElementById('alert-lon').value = '37.6173';
|
||
document.getElementById('alert-address').value = 'Test Location';
|
||
document.getElementById('alert-message').value = '🚨 WebSocket Test SOS Alert';
|
||
}
|
||
|
||
// ===== Monitoring Functions =====
|
||
function startAutoTest() {
|
||
if (state.autoTestRunning) return;
|
||
|
||
state.autoTestRunning = true;
|
||
showStatus('auto-test-status', '▶️ Auto test started - running every 30 seconds', 'success');
|
||
|
||
state.autoTestInterval = setInterval(() => {
|
||
if (state.connected) {
|
||
sendPing();
|
||
}
|
||
}, 30000);
|
||
|
||
// Run initial test
|
||
sendPing();
|
||
}
|
||
|
||
function stopAutoTest() {
|
||
if (state.autoTestInterval) {
|
||
clearInterval(state.autoTestInterval);
|
||
}
|
||
state.autoTestRunning = false;
|
||
showStatus('auto-test-status', '⏹️ Auto test stopped', 'info');
|
||
}
|
||
|
||
function updateStats() {
|
||
// Update uptime
|
||
if (state.startTime) {
|
||
const uptime = Math.floor((new Date() - state.startTime) / 1000);
|
||
document.getElementById('uptime').textContent = uptime + 's';
|
||
}
|
||
|
||
// Update connection status
|
||
document.getElementById('conn-status').textContent = state.connected ? 'ON' : 'OFF';
|
||
|
||
// Update performance metrics
|
||
if (state.latencies.length > 0) {
|
||
const avg = Math.round(state.latencies.reduce((a, b) => a + b) / state.latencies.length);
|
||
const max = Math.max(...state.latencies);
|
||
const min = Math.min(...state.latencies);
|
||
const rate = ((state.pongCount / state.pingCount) * 100).toFixed(1);
|
||
|
||
document.getElementById('avg-latency').textContent = avg + 'ms';
|
||
document.getElementById('max-latency').textContent = max + 'ms';
|
||
document.getElementById('min-latency').textContent = min + 'ms';
|
||
document.getElementById('success-rate').textContent = rate + '%';
|
||
}
|
||
|
||
// Update system stats
|
||
const stats = `
|
||
Connected: ${state.connected ? 'YES ✅' : 'NO ❌'}
|
||
Messages Received: ${state.messageCount}
|
||
Pings Sent: ${state.pingCount}
|
||
Pongs Received: ${state.pongCount}
|
||
Token Valid: ${state.token ? 'YES ✅' : 'NO ❌'}
|
||
Uptime: ${state.startTime ? Math.floor((new Date() - state.startTime) / 1000) + 's' : '0s'}
|
||
`.trim();
|
||
|
||
document.getElementById('system-stats').textContent = stats;
|
||
}
|
||
|
||
// ===== UI Helper Functions =====
|
||
function updateConnectionIndicator() {
|
||
const indicator = document.getElementById('ws-indicator');
|
||
const status = document.getElementById('ws-status');
|
||
|
||
if (state.connected) {
|
||
indicator.classList.remove('disconnected');
|
||
indicator.classList.add('connected');
|
||
status.textContent = '🟢 Connected';
|
||
} else {
|
||
indicator.classList.add('disconnected');
|
||
indicator.classList.remove('connected');
|
||
status.textContent = '🔴 Disconnected';
|
||
}
|
||
}
|
||
|
||
function showStatus(elementId, message, type) {
|
||
const element = document.getElementById(elementId);
|
||
element.textContent = message;
|
||
element.className = 'status show ' + type;
|
||
}
|
||
|
||
function addLog(message, type) {
|
||
const log = document.getElementById('ws-log');
|
||
const timestamp = new Date().toLocaleTimeString();
|
||
const entry = document.createElement('div');
|
||
entry.className = 'log-entry ' + type;
|
||
entry.innerHTML = `<span class="timestamp">[${timestamp}]</span> ${message}`;
|
||
log.appendChild(entry);
|
||
log.scrollTop = log.scrollHeight;
|
||
}
|
||
|
||
function clearLog() {
|
||
document.getElementById('ws-log').innerHTML = '';
|
||
}
|
||
|
||
// ===== Initialization =====
|
||
window.addEventListener('DOMContentLoaded', () => {
|
||
// Restore session
|
||
if (state.token && state.userId) {
|
||
displayTokenInfo();
|
||
updateWSUrl();
|
||
}
|
||
|
||
// Update stats every second
|
||
setInterval(updateStats, 1000);
|
||
|
||
// Show initial info
|
||
addLog('Tester loaded. Ready to test!', 'success');
|
||
});
|
||
|
||
// Cleanup on unload
|
||
window.addEventListener('beforeunload', () => {
|
||
if (state.ws) {
|
||
state.ws.close();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|