Merge branch 'main' of ssh://ssh.pstruebi.xyz:222/auracaster/bumble-auracast
This commit is contained in:
@@ -1,23 +1,88 @@
|
||||
# frontend/app.py
|
||||
import os
|
||||
import time
|
||||
import streamlit as st
|
||||
import requests
|
||||
from auracast import auracast_config
|
||||
import logging as log
|
||||
from PIL import Image
|
||||
|
||||
import requests
|
||||
from dotenv import load_dotenv
|
||||
import streamlit as st
|
||||
|
||||
from auracast import auracast_config
|
||||
from auracast.utils.frontend_auth import (
|
||||
is_pw_disabled,
|
||||
load_pw_record,
|
||||
save_pw_record,
|
||||
hash_password,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
# Set page configuration (tab title and icon) before using other Streamlit APIs
|
||||
# Always use the favicon from the utils folder relative to this file
|
||||
_THIS_DIR = os.path.dirname(__file__)
|
||||
_FAVICON_PATH = os.path.abspath(os.path.join(_THIS_DIR, '..', 'utils', 'favicon.ico'))
|
||||
favicon = Image.open(_FAVICON_PATH)
|
||||
st.set_page_config(page_title="Castbox", page_icon=favicon, layout="centered")
|
||||
|
||||
# Load environment variables from a .env file if present
|
||||
load_dotenv()
|
||||
|
||||
# Track whether WebRTC stream is active across Streamlit reruns
|
||||
if 'stream_started' not in st.session_state:
|
||||
st.session_state['stream_started'] = False
|
||||
|
||||
# Frontend authentication gate is controlled via env using shared utils
|
||||
|
||||
if 'frontend_authenticated' not in st.session_state:
|
||||
st.session_state['frontend_authenticated'] = False
|
||||
|
||||
if not is_pw_disabled():
|
||||
pw_rec = load_pw_record()
|
||||
|
||||
# First-time setup: no password set -> force user to choose one
|
||||
if pw_rec is None:
|
||||
st.header("Set up your frontend password")
|
||||
st.info("For security, you must set a password on first access.")
|
||||
with st.form("first_setup_form"):
|
||||
new_pw = st.text_input("New password", type="password")
|
||||
new_pw2 = st.text_input("Confirm password", type="password")
|
||||
submitted = st.form_submit_button("Save password")
|
||||
if submitted:
|
||||
if len(new_pw) < 6:
|
||||
st.error("Password should be at least 6 characters.")
|
||||
elif new_pw != new_pw2:
|
||||
st.error("Passwords do not match.")
|
||||
else:
|
||||
salt, key = hash_password(new_pw)
|
||||
try:
|
||||
save_pw_record(salt, key)
|
||||
st.success("Password saved. You can now sign in.")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"Failed to save password: {e}")
|
||||
st.stop()
|
||||
|
||||
# Normal sign-in gate
|
||||
if not st.session_state['frontend_authenticated']:
|
||||
st.header("Sign in")
|
||||
with st.form("signin_form"):
|
||||
pw = st.text_input("Password", type="password")
|
||||
submitted = st.form_submit_button("Sign in")
|
||||
if submitted:
|
||||
if verify_password(pw, pw_rec):
|
||||
st.session_state['frontend_authenticated'] = True
|
||||
st.success("Signed in.")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error("Incorrect password. Please try again.")
|
||||
# Stop rendering the rest of the app until authenticated
|
||||
if not st.session_state['frontend_authenticated']:
|
||||
st.stop()
|
||||
|
||||
# Global: desired packetization time in ms for Opus (should match backend)
|
||||
PTIME = 40
|
||||
BACKEND_URL = "http://localhost:5000"
|
||||
#TRANSPORT1 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_B53C372677E14460-if00,115200,rtscts"
|
||||
#TRANSPORT2 = "serial:/dev/serial/by-id/usb-ZEPHYR_Zephyr_HCI_UART_sample_CC69A2912F84AE5E-if00,115200,rtscts"
|
||||
|
||||
TRANSPORT1 = 'serial:/dev/ttyAMA3,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
TRANSPORT2 = 'serial:/dev/ttyAMA4,1000000,rtscts' # transport for raspberry pi gpio header
|
||||
QUALITY_MAP = {
|
||||
"High (48kHz)": {"rate": 48000, "octets": 120},
|
||||
"Good (32kHz)": {"rate": 32000, "octets": 80},
|
||||
@@ -34,19 +99,34 @@ try:
|
||||
except Exception:
|
||||
saved_settings = {}
|
||||
|
||||
st.title("🎙️ Auracast Audio Mode Control")
|
||||
# Define is_streaming early from the fetched status for use throughout the UI
|
||||
is_streaming = bool(saved_settings.get("is_streaming", False))
|
||||
|
||||
st.title("Auracast Audio Mode Control")
|
||||
|
||||
# Audio mode selection with persisted default
|
||||
options = ["Webapp", "USB/Network", "Demo"]
|
||||
saved_audio_mode = saved_settings.get("audio_mode", "Webapp")
|
||||
# Note: backend persists 'USB' for any device:<name> source (including AES67). We default to 'USB' in that case.
|
||||
options = [
|
||||
"Demo",
|
||||
"USB",
|
||||
"AES67",
|
||||
# "Webapp"
|
||||
]
|
||||
saved_audio_mode = saved_settings.get("audio_mode", "Demo")
|
||||
if saved_audio_mode not in options:
|
||||
saved_audio_mode = "Webapp"
|
||||
# Map legacy/unknown modes to closest
|
||||
mapping = {"USB/Network": "USB", "Network": "AES67"}
|
||||
saved_audio_mode = mapping.get(saved_audio_mode, "Demo")
|
||||
|
||||
audio_mode = st.selectbox(
|
||||
"Audio Mode",
|
||||
options,
|
||||
index=options.index(saved_audio_mode),
|
||||
help="Select the audio input source. Choose 'Webapp' for browser microphone, 'USB/Network' for a connected hardware device, or 'Demo' for a simulated stream."
|
||||
index=options.index(saved_audio_mode) if saved_audio_mode in options else options.index("Demo"),
|
||||
help=(
|
||||
"Select the audio input source. Choose 'Webapp' for browser microphone, "
|
||||
"'USB' for a connected USB audio device (via PipeWire), 'AES67' for network RTP/AES67 sources, "
|
||||
"or 'Demo' for a simulated stream."
|
||||
)
|
||||
)
|
||||
|
||||
if audio_mode == "Demo":
|
||||
@@ -66,15 +146,48 @@ if audio_mode == "Demo":
|
||||
index=0,
|
||||
help="Select the demo stream configuration."
|
||||
)
|
||||
# Stream password and flags (same as USB/AES67)
|
||||
saved_pwd = saved_settings.get('stream_password', '') or ''
|
||||
stream_passwort = st.text_input(
|
||||
"Stream Passwort",
|
||||
value=saved_pwd,
|
||||
type=("password"),
|
||||
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
|
||||
)
|
||||
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small")
|
||||
with col_flags1:
|
||||
assisted_listening = st.checkbox(
|
||||
"Assistive listening",
|
||||
value=bool(saved_settings.get('assisted_listening_stream', False))
|
||||
)
|
||||
with col_flags2:
|
||||
immediate_rendering = st.checkbox(
|
||||
"Immediate rendering",
|
||||
value=bool(saved_settings.get('immediate_rendering', False))
|
||||
)
|
||||
# QoS/presentation controls inline with flags
|
||||
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
|
||||
with col_pdelay:
|
||||
presentation_delay_us = st.number_input(
|
||||
"Presentation delay (µs)",
|
||||
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
|
||||
help="Delay between capture and presentation for receivers."
|
||||
)
|
||||
default_rtn = int(saved_settings.get('rtn', 4) or 4)
|
||||
with col_rtn:
|
||||
rtn = st.selectbox(
|
||||
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
|
||||
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
|
||||
)
|
||||
#st.info(f"Demo mode selected: {demo_selected} (Streams: {demo_stream_map[demo_selected]['streams']}, Rate: {demo_stream_map[demo_selected]['rate']} Hz)")
|
||||
# Start/Stop buttons for demo mode
|
||||
if 'demo_stream_started' not in st.session_state:
|
||||
st.session_state['demo_stream_started'] = False
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
start_demo = st.button("Start Demo Stream")
|
||||
start_demo = st.button("Start Demo Stream", disabled=is_streaming)
|
||||
with col2:
|
||||
stop_demo = st.button("Stop Demo Stream")
|
||||
stop_demo = st.button("Stop Demo Stream", disabled=not is_streaming)
|
||||
if start_demo:
|
||||
# Always stop any running stream for clean state
|
||||
try:
|
||||
@@ -99,6 +212,7 @@ if audio_mode == "Demo":
|
||||
for i in range(demo_cfg['streams']):
|
||||
cfg_cls, lang = lang_cfgs[i % len(lang_cfgs)]
|
||||
bigs1.append(cfg_cls(
|
||||
code=(stream_passwort.strip() or None),
|
||||
audio_source=f'file:../testdata/wave_particle_5min_{lang}_{int(q["rate"]/1000)}kHz_mono.wav',
|
||||
iso_que_len=32,
|
||||
sampling_frequency=q['rate'],
|
||||
@@ -115,7 +229,15 @@ if audio_mode == "Demo":
|
||||
config1 = auracast_config.AuracastConfigGroup(
|
||||
auracast_sampling_rate_hz=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
transport=TRANSPORT1,
|
||||
transport='', # is set in baccol_qoskend
|
||||
assisted_listening_stream=assisted_listening,
|
||||
immediate_rendering=immediate_rendering,
|
||||
presentation_delay_us=presentation_delay_us,
|
||||
qos_config=auracast_config.AuracastQoSConfig(
|
||||
iso_int_multiple_10ms=1,
|
||||
number_of_retransmissions=int(rtn),
|
||||
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||
),
|
||||
bigs=bigs1
|
||||
)
|
||||
config2 = None
|
||||
@@ -123,7 +245,15 @@ if audio_mode == "Demo":
|
||||
config2 = auracast_config.AuracastConfigGroup(
|
||||
auracast_sampling_rate_hz=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
transport=TRANSPORT2,
|
||||
transport='', # is set in backend
|
||||
assisted_listening_stream=assisted_listening,
|
||||
immediate_rendering=immediate_rendering,
|
||||
presentation_delay_us=presentation_delay_us,
|
||||
qos_config=auracast_config.AuracastQoSConfig(
|
||||
iso_int_multiple_10ms=1,
|
||||
number_of_retransmissions=int(rtn),
|
||||
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||
),
|
||||
bigs=bigs2
|
||||
)
|
||||
# Call /init and /init2
|
||||
@@ -151,6 +281,7 @@ if audio_mode == "Demo":
|
||||
st.session_state['demo_stream_started'] = False
|
||||
if r.get('was_running'):
|
||||
st.info("Demo stream stopped.")
|
||||
st.rerun()
|
||||
else:
|
||||
st.info("Demo stream was not running.")
|
||||
except Exception as e:
|
||||
@@ -194,61 +325,133 @@ else:
|
||||
value=default_lang,
|
||||
help="Three-letter language code (e.g., 'eng' for English, 'deu' for German). Used by receivers to display the language of the stream. See: https://en.wikipedia.org/wiki/List_of_ISO_639-3_codes"
|
||||
)
|
||||
# Optional broadcast code for coded streams
|
||||
stream_passwort = st.text_input(
|
||||
"Stream Passwort",
|
||||
value="",
|
||||
type="password",
|
||||
help="Optional: Set a broadcast code to protect your stream. Leave empty for an open (uncoded) broadcast."
|
||||
)
|
||||
# Flags and QoS row (compact, four columns)
|
||||
col_flags1, col_flags2, col_pdelay, col_rtn = st.columns([1, 1, 1, 1], gap="small")
|
||||
with col_flags1:
|
||||
assisted_listening = st.checkbox(
|
||||
"Assistive listening",
|
||||
value=bool(saved_settings.get('assisted_listening_stream', False))
|
||||
)
|
||||
with col_flags2:
|
||||
immediate_rendering = st.checkbox(
|
||||
"Immediate rendering",
|
||||
value=bool(saved_settings.get('immediate_rendering', False))
|
||||
)
|
||||
# QoS/presentation controls inline with flags
|
||||
default_pdelay = int(saved_settings.get('presentation_delay_us', 40000) or 40000)
|
||||
with col_pdelay:
|
||||
presentation_delay_us = st.number_input(
|
||||
"Presentation delay (µs)",
|
||||
min_value=10000, max_value=200000, step=1000, value=default_pdelay,
|
||||
help="Delay between capture and presentation for receivers."
|
||||
)
|
||||
default_rtn = int(saved_settings.get('rtn', 4) or 4)
|
||||
with col_rtn:
|
||||
rtn = st.selectbox(
|
||||
"Retransmissions (RTN)", options=[0,1,2,3,4], index=[0,1,2,3,4].index(default_rtn),
|
||||
help="Number of ISO retransmissions (higher improves robustness at cost of airtime)."
|
||||
)
|
||||
# Gain slider for Webapp mode
|
||||
if audio_mode == "Webapp":
|
||||
mic_gain = st.slider("Microphone Gain", 0.0, 2.0, 1.0, 0.1, help="Adjust microphone volume sent to Auracast")
|
||||
else:
|
||||
mic_gain = 1.0
|
||||
|
||||
# Input device selection for USB mode
|
||||
if audio_mode == "USB/Network":
|
||||
resp = requests.get(f"{BACKEND_URL}/audio_inputs")
|
||||
device_list = resp.json().get('inputs', [])
|
||||
# Display "name [id]" but use name as value
|
||||
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
|
||||
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
|
||||
device_names = [d['name'] for d in device_list]
|
||||
# Input device selection for USB or AES67 mode
|
||||
if audio_mode in ("USB", "AES67"):
|
||||
if not is_streaming:
|
||||
# Only query device lists when NOT streaming to avoid extra backend calls
|
||||
try:
|
||||
endpoint = "/audio_inputs_pw_usb" if audio_mode == "USB" else "/audio_inputs_pw_network"
|
||||
resp = requests.get(f"{BACKEND_URL}{endpoint}")
|
||||
device_list = resp.json().get('inputs', [])
|
||||
except Exception as e:
|
||||
st.error(f"Failed to fetch devices: {e}")
|
||||
device_list = []
|
||||
|
||||
# Determine default input by name
|
||||
default_input_name = saved_settings.get('input_device')
|
||||
if default_input_name not in device_names and device_names:
|
||||
default_input_name = device_names[0]
|
||||
default_input_label = None
|
||||
for label, name in option_name_map.items():
|
||||
if name == default_input_name:
|
||||
default_input_label = label
|
||||
break
|
||||
if not input_options:
|
||||
st.warning("No hardware audio input devices found. Plug in a USB input device and click Refresh.")
|
||||
if st.button("Refresh"):
|
||||
try:
|
||||
requests.post(f"{BACKEND_URL}/refresh_audio_inputs", timeout=3)
|
||||
except Exception as e:
|
||||
st.error(f"Failed to refresh devices: {e}")
|
||||
st.rerun()
|
||||
input_device = None
|
||||
else:
|
||||
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
|
||||
with col1:
|
||||
selected_option = st.selectbox(
|
||||
"Input Device",
|
||||
input_options,
|
||||
index=input_options.index(default_input_label) if default_input_label in input_options else 0
|
||||
# Display "name [id]" but use name as value
|
||||
input_options = [f"{d['name']} [{d['id']}]" for d in device_list]
|
||||
option_name_map = {f"{d['name']} [{d['id']}]": d['name'] for d in device_list}
|
||||
device_names = [d['name'] for d in device_list]
|
||||
|
||||
# Determine default input by name (from persisted server state)
|
||||
default_input_name = saved_settings.get('input_device')
|
||||
if default_input_name not in device_names and device_names:
|
||||
default_input_name = device_names[0]
|
||||
default_input_label = None
|
||||
for label, name in option_name_map.items():
|
||||
if name == default_input_name:
|
||||
default_input_label = label
|
||||
break
|
||||
if not input_options:
|
||||
warn_text = (
|
||||
"No USB audio input devices found. Connect a USB input and click Refresh."
|
||||
if audio_mode == "USB" else
|
||||
"No AES67/Network inputs found."
|
||||
)
|
||||
with col2:
|
||||
if st.button("Refresh"):
|
||||
st.warning(warn_text)
|
||||
if st.button("Refresh", disabled=is_streaming):
|
||||
try:
|
||||
requests.post(f"{BACKEND_URL}/refresh_audio_inputs", timeout=3)
|
||||
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
|
||||
if not r.ok:
|
||||
st.error(f"Failed to refresh: {r.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Failed to refresh devices: {e}")
|
||||
st.rerun()
|
||||
# Send only the device name to backend
|
||||
input_device = option_name_map[selected_option] if selected_option in option_name_map else None
|
||||
input_device = None
|
||||
else:
|
||||
col1, col2 = st.columns([3, 1], vertical_alignment="bottom")
|
||||
with col1:
|
||||
selected_option = st.selectbox(
|
||||
"Input Device",
|
||||
input_options,
|
||||
index=input_options.index(default_input_label) if default_input_label in input_options else 0
|
||||
)
|
||||
with col2:
|
||||
if st.button("Refresh", disabled=is_streaming):
|
||||
try:
|
||||
r = requests.post(f"{BACKEND_URL}/refresh_audio_devices", timeout=8)
|
||||
if not r.ok:
|
||||
st.error(f"Failed to refresh: {r.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Failed to refresh devices: {e}")
|
||||
st.rerun()
|
||||
# Send only the device name to backend
|
||||
input_device = option_name_map.get(selected_option)
|
||||
else:
|
||||
# When streaming, keep showing the current selection but lock editing.
|
||||
input_device = saved_settings.get('input_device')
|
||||
current_label = input_device or "No device selected"
|
||||
st.selectbox(
|
||||
"Input Device",
|
||||
[current_label],
|
||||
index=0,
|
||||
disabled=True,
|
||||
help="Stop the stream to change the input device."
|
||||
)
|
||||
else:
|
||||
input_device = None
|
||||
|
||||
start_stream = st.button("Start Auracast")
|
||||
stop_stream = st.button("Stop Auracast")
|
||||
# Buttons and status on a single row (4 columns: start, stop, spacer, status)
|
||||
c_start, c_stop, c_spacer, c_status = st.columns([1, 1, 1, 2], gap="small", vertical_alignment="center")
|
||||
with c_start:
|
||||
start_stream = st.button("Start Auracast", disabled=is_streaming)
|
||||
with c_stop:
|
||||
stop_stream = st.button("Stop Auracast", disabled=not is_streaming)
|
||||
# c_spacer intentionally left empty to push status to the far right
|
||||
with c_status:
|
||||
# Fetch current status from backend and render using Streamlit widgets (no HTML)
|
||||
# The is_streaming variable is now defined at the top of the script.
|
||||
# We only need to re-fetch here if we want the absolute latest status for the display,
|
||||
# but for UI consistency, we can just use the value from the top of the script run.
|
||||
st.write("🟢 Streaming" if is_streaming else "🔴 Stopped")
|
||||
|
||||
# If gain slider moved while streaming, send update to JS without restarting
|
||||
if audio_mode == "Webapp" and st.session_state.get('stream_started'):
|
||||
@@ -265,6 +468,7 @@ else:
|
||||
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
||||
if r['was_running']:
|
||||
st.success("Stream Stopped!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.success("Stream was not running.")
|
||||
except Exception as e:
|
||||
@@ -287,8 +491,8 @@ else:
|
||||
if start_stream:
|
||||
# Always send stop to ensure backend is in a clean state, regardless of current status
|
||||
r = requests.post(f"{BACKEND_URL}/stop_audio").json()
|
||||
if r['was_running']:
|
||||
st.success("Stream Stopped!")
|
||||
#if r['was_running']:
|
||||
# st.success("Stream Stopped!")
|
||||
|
||||
# Small pause lets backend fully release audio devices before re-init
|
||||
time.sleep(1)
|
||||
@@ -297,18 +501,27 @@ else:
|
||||
config = auracast_config.AuracastConfigGroup(
|
||||
auracast_sampling_rate_hz=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
transport=TRANSPORT1, # transport for raspberry pi gpio header
|
||||
transport='', # is set in backend
|
||||
assisted_listening_stream=assisted_listening,
|
||||
immediate_rendering=immediate_rendering,
|
||||
presentation_delay_us=presentation_delay_us,
|
||||
qos_config=auracast_config.AuracastQoSConfig(
|
||||
iso_int_multiple_10ms=1,
|
||||
number_of_retransmissions=int(rtn),
|
||||
max_transport_latency_ms=int(rtn)*10 + 3,
|
||||
),
|
||||
bigs = [
|
||||
auracast_config.AuracastBigConfig(
|
||||
code=(stream_passwort.strip() or None),
|
||||
name=stream_name,
|
||||
program_info=program_info,
|
||||
language=language,
|
||||
audio_source=(
|
||||
f"device:{input_device}" if audio_mode == "USB/Network" else (
|
||||
f"device:{input_device}" if audio_mode in ("USB", "AES67") else (
|
||||
"webrtc" if audio_mode == "Webapp" else "network"
|
||||
)
|
||||
),
|
||||
input_format=(f"int16le,{q['rate']},1" if audio_mode == "USB/Network" else "auto"),
|
||||
input_format=(f"int16le,{q['rate']},1" if audio_mode in ("USB", "AES67") else "auto"),
|
||||
iso_que_len=1,
|
||||
sampling_frequency=q['rate'],
|
||||
octets_per_frame=q['octets'],
|
||||
@@ -320,6 +533,7 @@ else:
|
||||
r = requests.post(f"{BACKEND_URL}/init", json=config.model_dump())
|
||||
if r.status_code == 200:
|
||||
st.success("Stream Started!")
|
||||
st.rerun()
|
||||
else:
|
||||
st.error(f"Failed to initialize: {r.text}")
|
||||
except Exception as e:
|
||||
@@ -393,6 +607,47 @@ else:
|
||||
# else:
|
||||
# st.error("Could not fetch advertised streams.")
|
||||
|
||||
############################
|
||||
# System expander (collapsed)
|
||||
############################
|
||||
with st.expander("System control", expanded=False):
|
||||
|
||||
st.subheader("Change password")
|
||||
if is_pw_disabled():
|
||||
st.info("Frontend password protection is disabled via DISABLE_FRONTEND_PW.")
|
||||
else:
|
||||
with st.form("change_pw_form"):
|
||||
cur = st.text_input("Current password", type="password")
|
||||
new1 = st.text_input("New password", type="password")
|
||||
new2 = st.text_input("Confirm new password", type="password")
|
||||
submit_change = st.form_submit_button("Change password")
|
||||
if submit_change:
|
||||
rec = load_pw_record()
|
||||
if not rec or not verify_password(cur, rec):
|
||||
st.error("Current password is incorrect.")
|
||||
elif len(new1) < 6:
|
||||
st.error("New password should be at least 6 characters.")
|
||||
elif new1 != new2:
|
||||
st.error("New passwords do not match.")
|
||||
else:
|
||||
salt, key = hash_password(new1)
|
||||
try:
|
||||
save_pw_record(salt, key)
|
||||
st.success("Password updated.")
|
||||
except Exception as e:
|
||||
st.error(f"Failed to update password: {e}")
|
||||
|
||||
st.subheader("Reboot")
|
||||
if st.button("Reboot now", type="primary"):
|
||||
try:
|
||||
r = requests.post(f"{BACKEND_URL}/system_reboot", timeout=1)
|
||||
if r.ok:
|
||||
st.success("Reboot initiated. The UI will become unreachable shortly.")
|
||||
else:
|
||||
st.error(f"Failed to reboot: {r.status_code} {r.text}")
|
||||
except Exception as e:
|
||||
st.error(f"Error calling reboot: {e}")
|
||||
|
||||
log.basicConfig(
|
||||
level=os.environ.get('LOG_LEVEL', log.DEBUG),
|
||||
format='%(module)s.py:%(lineno)d %(levelname)s: %(message)s'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user