baton/frontend/app.js
2026-03-20 21:09:05 +02:00

264 lines
6.5 KiB
JavaScript

'use strict';
// ========== Storage abstraction (decisions #1024, #1025) ==========
//
// #1024: probe availability with a real write, NOT typeof/in
// #1025: fallback chain: localStorage → sessionStorage → in-memory
let _storage = null;
let _storageType = 'memory';
const _mem = {};
function _probeStorage(s) {
try {
const k = '__baton_probe__';
s.setItem(k, '1');
s.removeItem(k);
return true;
} catch (_) {
return false;
}
}
function _initStorage() {
if (_probeStorage(localStorage)) {
_storage = localStorage;
_storageType = 'local';
} else if (_probeStorage(sessionStorage)) {
_storage = sessionStorage;
_storageType = 'session';
} else {
_storage = {
getItem: (k) => (Object.prototype.hasOwnProperty.call(_mem, k) ? _mem[k] : null),
setItem: (k, v) => { _mem[k] = String(v); },
removeItem: (k) => { delete _mem[k]; },
};
_storageType = 'memory';
}
}
// ========== User identity ==========
function _getOrCreateUserId() {
let id = _storage.getItem('baton_user_id');
if (!id) {
id = crypto.randomUUID();
_storage.setItem('baton_user_id', id);
}
return id;
}
function _isRegistered() {
return _storage.getItem('baton_registered') === '1';
}
function _getUserName() {
return _storage.getItem('baton_user_name') || '';
}
function _saveRegistration(name) {
_storage.setItem('baton_user_name', name);
_storage.setItem('baton_registered', '1');
}
function _getInitials(name) {
const trimmed = (name || '').trim();
if (!trimmed) return '?';
return trimmed
.split(/\s+/)
.map((w) => w[0] || '')
.join('')
.toUpperCase()
.slice(0, 2);
}
// ========== UI helpers ==========
function _showScreen(id) {
document.querySelectorAll('.screen').forEach((el) => {
el.hidden = el.id !== id;
});
}
function _setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status' + (cls ? ' status--' + cls : '');
el.hidden = !msg;
}
function _updateNetworkIndicator() {
const el = document.getElementById('indicator-network');
if (!el) return;
el.className = 'network-indicator ' + (navigator.onLine ? 'online' : 'offline');
el.setAttribute('aria-label', navigator.onLine ? 'Online' : 'Offline');
}
function _updateUserAvatar() {
const el = document.getElementById('user-avatar');
if (!el) return;
el.textContent = _getInitials(_getUserName());
}
// ========== API calls ==========
async function _apiPost(path, body) {
const res = await fetch(path, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!res.ok) {
const text = await res.text().catch(() => '');
throw new Error('HTTP ' + res.status + (text ? ': ' + text : ''));
}
return res.json();
}
// ========== Geolocation (optional, non-blocking) ==========
function _getGeo() {
return new Promise((resolve) => {
if (!navigator.geolocation) { resolve(null); return; }
navigator.geolocation.getCurrentPosition(
({ coords }) =>
resolve({
lat: coords.latitude,
lon: coords.longitude,
accuracy: coords.accuracy,
}),
() => resolve(null),
{ timeout: 5000, maximumAge: 30000 }
);
});
}
// ========== Handlers ==========
async function _handleRegister() {
const input = document.getElementById('name-input');
const btn = document.getElementById('btn-confirm');
const name = input.value.trim();
if (!name) return;
btn.disabled = true;
_setStatus('', '');
try {
const uuid = _getOrCreateUserId();
await _apiPost('/api/register', { uuid, name });
_saveRegistration(name);
_updateUserAvatar();
_showMain();
} catch (_) {
_setStatus('Error. Please try again.', 'error');
btn.disabled = false;
}
}
function _setSosState(state) {
const btn = document.getElementById('btn-sos');
if (!btn) return;
btn.dataset.state = state;
btn.disabled = state === 'sending';
}
async function _handleSignal() {
// v1: no offline queue — show error and return (decision #1019)
if (!navigator.onLine) {
_setStatus('No connection. Check your network and try again.', 'error');
return;
}
_setSosState('sending');
_setStatus('', '');
try {
const geo = await _getGeo();
const uuid = _getOrCreateUserId();
const body = { user_id: uuid, timestamp: Date.now() };
if (geo) body.geo = geo;
await _apiPost('/api/signal', body);
_setSosState('success');
_setStatus('Signal sent!', 'success');
setTimeout(() => {
_setSosState('default');
_setStatus('', '');
}, 2000);
} catch (_) {
_setSosState('default');
_setStatus('Error sending. Try again.', 'error');
}
}
// ========== Screens ==========
function _showOnboarding() {
_showScreen('screen-onboarding');
const input = document.getElementById('name-input');
const btn = document.getElementById('btn-confirm');
input.addEventListener('input', () => {
btn.disabled = input.value.trim().length === 0;
});
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && !btn.disabled) _handleRegister();
});
btn.addEventListener('click', _handleRegister);
}
function _showMain() {
_showScreen('screen-main');
_updateUserAvatar();
const btn = document.getElementById('btn-sos');
if (btn && !btn.dataset.listenerAttached) {
btn.addEventListener('click', _handleSignal);
btn.dataset.listenerAttached = '1';
}
}
// ========== Service Worker ==========
function _registerSW() {
if (!('serviceWorker' in navigator)) return;
navigator.serviceWorker.register('/sw.js').catch((err) => {
console.warn('[baton] SW registration failed:', err);
});
}
// ========== Init ==========
function _init() {
_initStorage();
// Pre-generate and persist UUID on first visit (per arch spec flow)
_getOrCreateUserId();
// Private mode graceful degradation (decision #1041):
// show inline banner with explicit action guidance when localStorage is unavailable
if (_storageType !== 'local') {
const banner = document.getElementById('private-mode-banner');
if (banner) banner.hidden = false;
}
// Network indicator — initial state + live updates
_updateNetworkIndicator();
window.addEventListener('online', _updateNetworkIndicator);
window.addEventListener('offline', _updateNetworkIndicator);
// Route to correct screen
if (_isRegistered()) {
_showMain();
} else {
_showOnboarding();
}
}
document.addEventListener('DOMContentLoaded', () => {
_registerSW();
_init();
});