"""Admin review console โ€” list submissions, view review timeline, add reviews. Submissions are immutable. Each review is stored as its own file under ``reviews//``; a submission can be reviewed many times by different people. The "current status" shown here is the most recent review's status. """ from __future__ import annotations import json import streamlit as st from lib.schema import VALID_STATUSES from lib.storage import ( ADMIN_PASSWORD, add_review, check_admin_password, hf_configured, list_submissions, ) st.set_page_config(page_title="TDB Intake โ€” Admin", page_icon="๐Ÿ”", layout="wide") STATUS_EMOJI = { "pending": "๐ŸŸก", "reviewed": "๐ŸŸข", "needs_fix": "๐Ÿ”ด", } def status_badge(status: str) -> str: return f"{STATUS_EMOJI.get(status, 'โšช')} {status}" # ------------- auth ------------------------------------------------------ if "admin_authed" not in st.session_state: st.session_state.admin_authed = False st.title("Admin โ€” Review submissions") if not ADMIN_PASSWORD: st.warning("`ADMIN_PASSWORD` is not set on the server โ€” admin is open to anyone.") st.session_state.admin_authed = True if not st.session_state.admin_authed: with st.form("auth"): pw = st.text_input("Admin password", type="password") if st.form_submit_button("Continue"): if check_admin_password(pw): st.session_state.admin_authed = True st.rerun() else: st.error("Wrong password.") st.stop() # ------------- load list ------------------------------------------------- if not hf_configured: st.info("โ„น๏ธ Reading from `./data/` (local dev mode).") cols = st.columns([3, 1]) with cols[0]: status_filter = st.multiselect( "Filter by status", options=VALID_STATUSES, default=[], placeholder="Show all", ) with cols[1]: if st.button("Refresh", use_container_width=True): st.rerun() try: items = list_submissions() except Exception as e: st.error(f"Failed to list submissions: {e}") st.stop() if status_filter: items = [s for s in items if s["status"] in status_filter] st.caption(f"{len(items)} submission(s)") if not items: st.info("No submissions match this filter.") st.stop() # ------------- list display ---------------------------------------------- for s in items: n_reviews = s.get("review_count", 0) review_tag = f" ยท ๐Ÿ’ฌ {n_reviews}" if n_reviews else " ยท no reviews yet" label = ( f"{STATUS_EMOJI.get(s['status'], 'โšช')} " f"**{s['trial_id']}** โ€” {s['username']} ยท " f"_{s['submittedAt']}_{review_tag}" ) with st.expander(label): meta_c1, meta_c2 = st.columns(2) with meta_c1: st.markdown( f"**Current status:** {status_badge(s.get('status', 'pending'))} \n" f"**Submitted:** {s.get('submittedAt', '')} \n" f"**Last reviewed:** {s.get('reviewedAt', '') or 'โ€”'}" ) with meta_c2: st.markdown( f"**File:** `{s.get('submissionId', '')}` \n" f"**Last reviewer:** {s.get('reviewer', '') or 'โ€”'}" ) # ---- Review timeline ----------------------------------------- reviews = s.get("reviews") or [] st.markdown(f"#### Review history ({len(reviews)})") if not reviews: st.caption("No reviews yet.") else: for rev in reversed(reviews): # newest first st.markdown( f"- {status_badge(rev.get('status', ''))} โ€” " f"**{rev.get('reviewer') or 'anon'}** " f"ยท _{rev.get('at', '')}_" + (f" \n {rev.get('note')}" if rev.get("note") else "") ) # ---- Add a review -------------------------------------------- st.markdown("#### Add a review") with st.form(f"review_{s['submissionId']}"): new_status = st.radio( "Status", options=VALID_STATUSES, index=VALID_STATUSES.index(s.get("status", "pending")) if s.get("status") in VALID_STATUSES else 0, horizontal=True, ) rc1, rc2 = st.columns([1, 2]) with rc1: reviewer = st.text_input("Your name", placeholder="e.g., Dr. Smith") with rc2: note = st.text_input("Comment", placeholder="optional") if st.form_submit_button("Add review", type="primary"): if not reviewer.strip(): st.error("Please enter your name.") else: try: add_review( s["submissionId"], status=new_status, reviewer=reviewer.strip(), note=note.strip(), ) st.success("Review added.") st.rerun() except Exception as e: st.error(f"Failed to add review: {e}") with st.expander("Raw submission JSON"): st.code(json.dumps(s.get("submission", {}), indent=2, ensure_ascii=False), language="json")