@@ -10,11 +10,13 @@ import { Badge } from "@/components/Badge/Badge";
1010import { Panel } from "@/components/Panel/Panel" ;
1111import { VotePanel , type VoteState } from "@/components/VotePanel/VotePanel" ;
1212import {
13+ apiFetch ,
1314 apiUrl ,
1415 startVoteRound ,
1516 tally as tallyVote ,
1617 getTally ,
1718 endVoteRound ,
19+ getSessionIds ,
1820 type TallyResult ,
1921} from "@/signatures/voteSession" ;
2022import {
@@ -224,12 +226,14 @@ function QRPanel({
224226function VoterListPanel ( {
225227 voters,
226228 loading,
229+ selfUuuid,
227230 onRemove,
228231 onRemoveAll,
229232 onReload,
230233} : {
231234 voters : VoterInfo [ ] ;
232235 loading : boolean ;
236+ selfUuuid : string | null ;
233237 onRemove : ( uuid : string ) => Promise < void > ;
234238 onRemoveAll : ( ) => Promise < void > ;
235239 onReload : ( ) => void ;
@@ -365,7 +369,7 @@ function VoterListPanel({
365369 </ Badge >
366370 ) }
367371
368- { ! v . is_host && (
372+ { v . uuid !== selfUuuid && (
369373 < button
370374 type = "button"
371375 onClick = { ( ) => handleRemove ( v . uuid ) }
@@ -922,6 +926,8 @@ function HostVoteRoundPanel({
922926
923927// ─── Admin page ───────────────────────────────────────────────────────────────
924928
929+ const SESSION_POLL_MS = 10_000 ;
930+
925931function Admin ( ) {
926932 const navigate = useNavigate ( ) ;
927933 const [ voters , setVoters ] = useState < VoterInfo [ ] > ( [ ] ) ;
@@ -942,6 +948,9 @@ function Admin() {
942948 null ,
943949 ) ;
944950 const [ loadError , setLoadError ] = useState < string | null > ( null ) ;
951+ const [ selfUuuid , setSelfUuuid ] = useState < string | null > ( null ) ;
952+ // null = still checking, true = in meeting, false = removed/not logged in
953+ const [ sessionValid , setSessionValid ] = useState < boolean | null > ( null ) ;
945954
946955 const reloadVoters = useCallback ( async ( ) => {
947956 try {
@@ -952,23 +961,46 @@ function Admin() {
952961 }
953962 } , [ ] ) ;
954963
964+ const refreshSession = useCallback ( async ( ) => {
965+ let res : Response ;
966+ try {
967+ res = await apiFetch ( "/api/common/vote-progress" ) ;
968+ } catch {
969+ return ; // network error — don't invalidate session
970+ }
971+ if ( res . status === 401 ) {
972+ setSessionValid ( false ) ;
973+ return ;
974+ }
975+ if ( ! res . ok ) return ; // other server error — keep current session state
976+ setSessionValid ( true ) ;
977+ } , [ ] ) ;
978+
955979 // ── Initial load ────────────────────────────────────────────────────────────
956980 useEffect ( ( ) => {
957981 async function init ( ) {
958982 try {
959- const [ voterList , progress ] = await Promise . all ( [
983+ const [ voterList , progress , sessionIds ] = await Promise . all ( [
960984 fetchVoterList ( ) ,
961985 fetchVoteProgress ( ) ,
986+ getSessionIds ( ) ,
962987 ] ) ;
963988 setVoters ( voterList ) ;
989+ setSelfUuuid ( sessionIds . uuuid ) ;
964990 setVoteProgress ( progress ) ;
965991 const state = deriveVoteState ( progress ) ;
966992 setVoteState ( state ) ;
993+ setSessionValid ( true ) ;
967994 if ( state === "Tally" ) {
968995 getTally ( ) . then ( setTallyResult ) . catch ( console . error ) ;
969996 }
970997 } catch ( err ) {
971- setLoadError ( String ( err ) ) ;
998+ if ( err instanceof Error && err . message . includes ( "401" ) ) {
999+ setSessionValid ( false ) ;
1000+ } else {
1001+ setSessionValid ( true ) ;
1002+ setLoadError ( String ( err ) ) ;
1003+ }
9721004 } finally {
9731005 setVotersLoading ( false ) ;
9741006 }
@@ -1048,6 +1080,12 @@ function Admin() {
10481080 return ( ) => es . close ( ) ;
10491081 } , [ reloadVoters ] ) ;
10501082
1083+ // ── Periodic session check ───────────────────────────────────────────────────
1084+ useEffect ( ( ) => {
1085+ const timer = setInterval ( refreshSession , SESSION_POLL_MS ) ;
1086+ return ( ) => clearInterval ( timer ) ;
1087+ } , [ refreshSession ] ) ;
1088+
10511089 // ── Handlers ────────────────────────────────────────────────────────────────
10521090
10531091 async function handleStartVote (
@@ -1125,6 +1163,28 @@ function Admin() {
11251163
11261164 // ── Render ──────────────────────────────────────────────────────────────────
11271165
1166+ if ( sessionValid === null ) {
1167+ return (
1168+ < div className = "flex items-center justify-center py-32" >
1169+ < Spinner size = "l" color = "primary" />
1170+ </ div >
1171+ ) ;
1172+ }
1173+
1174+ if ( ! sessionValid ) {
1175+ return (
1176+ < div className = "max-w-md mx-auto px-6 py-10" >
1177+ < Panel title = "Not an administrator" >
1178+ < p className = "text-sm" style = { { color : "var(--textSecondary)" } } >
1179+ You are not currently an administrator in a meeting. You may have
1180+ been removed or your session may have expired. If you believe this
1181+ is a mistake, please contact your meeting administrator.
1182+ </ p >
1183+ </ Panel >
1184+ </ div >
1185+ ) ;
1186+ }
1187+
11281188 if ( loadError ) {
11291189 return (
11301190 < div className = "max-w-xl mx-auto p-8" >
@@ -1287,6 +1347,7 @@ function Admin() {
12871347 < VoterListPanel
12881348 voters = { voters }
12891349 loading = { votersLoading }
1350+ selfUuuid = { selfUuuid }
12901351 onRemove = { removeVoter }
12911352 onRemoveAll = { removeAllVoters }
12921353 onReload = { reloadVoters }
0 commit comments