dispatch-console / web /src /App.jsx
secutorpro's picture
app shell: auth, realtime wiring, layout
2b8468d verified
Raw History Blame Contribute Delete
4.98 kB
import { useCallback, useEffect, useRef, useState } from 'react';
import { api, getToken, setToken } from './api.js';
import { DispatchSocket } from './ws.js';
import { PttPlayer, startCapture, sosBeep } from './audio.js';
import MapView from './components/MapView.jsx';
import SosPanel from './components/SosPanel.jsx';
import MembersPanel from './components/MembersPanel.jsx';
import PttPanel from './components/PttPanel.jsx';
import Login from './components/Login.jsx';
export default function App() {
const [user, setUser] = useState(null);
const [members, setMembers] = useState([]);
const [groups, setGroups] = useState([]);
const [sos, setSos] = useState([]);
const [floors, setFloors] = useState({});
const [connected, setConnected] = useState(false);
const [tab, setTab] = useState('members');
const [selectedGroup, setSelectedGroup] = useState(null);
const [talking, setTalking] = useState(false);
const [muted, setMuted] = useState(false);
const sockRef = useRef(null);
const playerRef = useRef(null);
const captureRef = useRef(null);
const mutedRef = useRef(false);
mutedRef.current = muted;
// restore session on reload
useEffect(() => {
if (!getToken()) return;
api.get('/me').then(setUser).catch(() => setToken(''));
}, []);
const startSocket = useCallback(() => {
sockRef.current?.close();
sockRef.current = new DispatchSocket({
onStatus: setConnected,
onWelcome: (d) => {
setGroups(d.groups);
setMembers(d.members);
setSos(d.sos);
setFloors(Object.fromEntries(d.floors.map((f) => [f.groupId, f])));
},
onMembers: setMembers,
onGroups: setGroups,
onSosNew: (alert) => {
setSos((prev) => [alert, ...prev.filter((a) => a.id !== alert.id)]);
if (!mutedRef.current) sosBeep(4);
},
onSosUpdate: (alert) => setSos((prev) => prev.map((a) => (a.id === alert.id ? alert : a))),
onFloor: (f) =>
setFloors((prev) => {
if (f.memberId == null) {
const { [f.groupId]: _drop, ...rest } = prev;
return rest;
}
return { ...prev, [f.groupId]: f };
}),
onAudio: ({ memberId, pcm }) => {
if (mutedRef.current || memberId === 0) return;
if (!playerRef.current) playerRef.current = new PttPlayer();
playerRef.current.play(pcm);
},
});
}, []);
useEffect(() => {
if (user && !sockRef.current) startSocket();
}, [user, startSocket]);
const handleLogin = async (username, password) => {
const { token, user: u } = await api.post('/login', { username, password });
setToken(token);
setUser(u);
};
const logout = () => {
sockRef.current?.close();
sockRef.current = null;
captureRef.current?.stop();
captureRef.current = null;
setToken('');
setUser(null);
};
const beginTalk = async () => {
if (!selectedGroup || talking) return;
try {
captureRef.current = await startCapture((pcm) => sockRef.current?.sendAudio(selectedGroup, 0, pcm));
sockRef.current?.send({ type: 'ptt_start', groupId: selectedGroup });
setTalking(true);
} catch (err) {
console.error('could not start microphone:', err);
}
};
const endTalk = () => {
sockRef.current?.send({ type: 'ptt_stop' });
captureRef.current?.stop();
captureRef.current = null;
setTalking(false);
};
const ackSos = (id) => api.post(`/sos/${id}/ack`).catch((e) => console.error(e));
const resolveSos = (id) => api.post(`/sos/${id}/resolve`).catch((e) => console.error(e));
if (!user) return <Login onLogin={handleLogin} />;
const openCount = sos.filter((a) => a.state === 'open').length;
return (
<div className="app">
<header>
<div className="brand">Dispatch Console</div>
<div className={`conn ${connected ? 'on' : 'off'}`}>{connected ? 'live' : 'offline'}</div>
<span className="muted">{user.username}</span>
<button className="secondary" onClick={logout}>Log out</button>
</header>
<aside>
<SosPanel sos={sos} openCount={openCount} onAck={ackSos} onResolve={resolveSos} />
<div className="tabs">
<button className={tab === 'members' ? 'active' : ''} onClick={() => setTab('members')}>Members</button>
<button className={tab === 'ptt' ? 'active' : ''} onClick={() => setTab('ptt')}>Intercom</button>
</div>
{tab === 'members' ? (
<MembersPanel members={members} groups={groups} />
) : (
<PttPanel
groups={groups}
floors={floors}
selected={selectedGroup}
setSelected={setSelectedGroup}
onTalkStart={beginTalk}
onTalkStop={endTalk}
talking={talking}
muted={muted}
setMuted={setMuted}
/>
)}
</aside>
<MapView members={members} groups={groups} sos={sos} />
</div>
);
}