feat: test signal via avatar/indicator tap on main screen

Tapping user avatar or network indicator sends a test signal with
geo data. Backend formats it as "Тест от username" (🧪) instead of
"Сигнал" (🚨). Only active after login on main screen.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gros Frumos 2026-03-21 16:06:02 +02:00
parent 0562cb4e47
commit 268fb62bf3
3 changed files with 54 additions and 5 deletions

View file

@ -229,11 +229,18 @@ async def signal(
if geo if geo
else "Гео нету" else "Гео нету"
) )
text = ( if body.is_test:
f"🚨 Сигнал от {user_name}\n" text = (
f"{ts.strftime('%H:%M:%S')} UTC\n" f"🧪 Тест от {user_name}\n"
f"{geo_info}" f"{ts.strftime('%H:%M:%S')} UTC\n"
) f"{geo_info}"
)
else:
text = (
f"🚨 Сигнал от {user_name}\n"
f"{ts.strftime('%H:%M:%S')} UTC\n"
f"{geo_info}"
)
asyncio.create_task(telegram.send_message(text)) asyncio.create_task(telegram.send_message(text))
return SignalResponse(status="ok", signal_id=signal_id) return SignalResponse(status="ok", signal_id=signal_id)

View file

@ -25,6 +25,7 @@ class SignalRequest(BaseModel):
user_id: Optional[str] = None # UUID for legacy api_key auth; omit for JWT auth user_id: Optional[str] = None # UUID for legacy api_key auth; omit for JWT auth
timestamp: int = Field(..., gt=0) timestamp: int = Field(..., gt=0)
geo: Optional[GeoData] = None geo: Optional[GeoData] = None
is_test: bool = False
class SignalResponse(BaseModel): class SignalResponse(BaseModel):

View file

@ -203,6 +203,33 @@ function _setSosState(state) {
btn.disabled = state === 'sending'; btn.disabled = state === 'sending';
} }
async function _handleTestSignal() {
if (!navigator.onLine) {
_setStatus('Нет соединения.', 'error');
return;
}
const token = _getAuthToken();
if (!token) return;
_setStatus('', '');
try {
const geo = await _getGeo();
const body = { timestamp: Date.now(), is_test: true };
if (geo) body.geo = geo;
await _apiPost('/api/signal', body, { Authorization: 'Bearer ' + token });
_setStatus('Тест отправлен', 'success');
setTimeout(() => _setStatus('', ''), 1500);
} catch (err) {
if (err && err.status === 401) {
_clearAuth();
_setStatus('Сессия истекла. Войдите заново.', 'error');
setTimeout(() => _showOnboarding(), 1500);
} else {
_setStatus('Ошибка отправки.', 'error');
}
}
}
async function _handleSignal() { async function _handleSignal() {
if (!navigator.onLine) { if (!navigator.onLine) {
_setStatus('Нет соединения. Проверьте сеть и попробуйте снова.', 'error'); _setStatus('Нет соединения. Проверьте сеть и попробуйте снова.', 'error');
@ -297,6 +324,20 @@ function _showMain() {
btn.addEventListener('click', _handleSignal); btn.addEventListener('click', _handleSignal);
btn.dataset.listenerAttached = '1'; btn.dataset.listenerAttached = '1';
} }
// Avatar and network indicator → test signal (only on main screen)
const avatar = document.getElementById('user-avatar');
if (avatar && !avatar.dataset.testAttached) {
avatar.addEventListener('click', _handleTestSignal);
avatar.dataset.testAttached = '1';
avatar.style.cursor = 'pointer';
}
const indicator = document.getElementById('indicator-network');
if (indicator && !indicator.dataset.testAttached) {
indicator.addEventListener('click', _handleTestSignal);
indicator.dataset.testAttached = '1';
indicator.style.cursor = 'pointer';
}
} }
// ========== Service Worker ========== // ========== Service Worker ==========