rename sidebar and tmux remote

This commit is contained in:
firestar5683
2026-03-07 13:09:36 -06:00
parent f14ac063b3
commit bd7f47eb20
5 changed files with 169 additions and 24 deletions
@@ -15,7 +15,7 @@ const MenuItems = {
{ name: "Screen Recordings", link: "/screen_recordings", icon: "bi-record-circle" },
],
tools: [
{ name: "Device Settings", link: "/device_settings", icon: "bi-sliders" },
{ name: "Toggles", link: "/device_settings", icon: "bi-toggle-on" },
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
{ name: "Galaxy", link: "/galaxy", icon: "bi-globe2" },
@@ -24,7 +24,7 @@ const MenuItems = {
{ name: "Testing Ground", link: "/testing_ground", icon: "bi-bezier2" },
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
{ name: "Backup and Restore", link: "/manage_toggles", icon: "bi-arrow-repeat" },
{ name: "Software", link: "/manage_updates", icon: "bi-arrow-up-circle" },
{ name: "Vehicle Features", link: "/vehicle_features", icon: "bi-car-front" },
],
@@ -617,7 +617,7 @@ export function DeviceSettings({ params }) {
return html`
<div class="ds-wrapper">
<h2>Device Settings</h2>
<h2>Toggles</h2>
<input
class="ds-search"
@@ -12,6 +12,16 @@ const logSelectorState = reactive({
newName: ""
});
let liveEventSource = null;
let livePollTimer = null;
let tmuxLifecycleVersion = 0;
const REMOTE_POLL_INTERVAL_MS = 2000;
const TRANSPORT_LIFECYCLE_INTERVAL_MS = 500;
function isTmuxRouteActive() {
return window.location.pathname === "/manage_tmux";
}
async function loadTmuxLogs() {
if (logSelectorState.loading || logSelectorState.logsLoadedOnce) return;
@@ -177,35 +187,145 @@ function TmuxLogSelector({ action, closeFn }) {
}
export function TmuxLog() {
if (isGalaxyTunnel()) {
return html`
<div class="tunnel-notice">
<div class="tunnel-notice-icon">🛰</div>
<h3 class="tunnel-notice-title">Tmux Log Unavailable via Galaxy</h3>
<p class="tunnel-notice-body">Live tmux streaming requires a direct connection.<br>Connect to your device's local network to use this feature.</p>
</div>
`;
}
const state = reactive({
paused: false,
latest: '',
log: '',
selectorAction: null,
transport: "connecting",
});
const lifecycleVersion = ++tmuxLifecycleVersion;
let lifecycleTimer = null;
const event_source = new EventSource("/api/tmux_log/live");
event_source.onmessage = e => {
state.latest = e.data;
function applyIncomingLog(data) {
state.latest = data || "";
if (!state.paused) {
state.log = state.latest;
}
}
event_source.onerror = err => {
console.error("Error receiving tmux log:", err);
event_source.close();
function stopLiveTransport() {
if (liveEventSource) {
liveEventSource.close();
liveEventSource = null;
}
if (livePollTimer) {
clearInterval(livePollTimer);
livePollTimer = null;
}
}
async function fetchSnapshot() {
try {
const response = await fetch("/api/tmux_log/snapshot");
if (!response.ok) throw new Error(await response.text());
const payload = await response.json();
applyIncomingLog(payload?.data || "");
} catch (err) {
console.error("Error fetching tmux log snapshot:", err);
}
}
function startPolling() {
if (livePollTimer) return;
if (liveEventSource) {
liveEventSource.close();
liveEventSource = null;
}
state.transport = "polling";
fetchSnapshot();
livePollTimer = setInterval(() => {
if (!isTmuxRouteActive()) {
stopLiveTransport();
return;
}
if (document.visibilityState !== "visible") {
return;
}
fetchSnapshot();
}, REMOTE_POLL_INTERVAL_MS);
}
function startEventStream() {
if (liveEventSource) return;
if (livePollTimer) {
clearInterval(livePollTimer);
livePollTimer = null;
}
state.transport = "streaming";
liveEventSource = new EventSource("/api/tmux_log/live");
let receivedAnyMessage = false;
liveEventSource.onmessage = e => {
if (!isTmuxRouteActive() || document.visibilityState !== "visible") return;
receivedAnyMessage = true;
applyIncomingLog(e.data);
};
liveEventSource.onerror = err => {
console.error("Error receiving tmux log stream:", err);
if (!isTmuxRouteActive()) {
stopLiveTransport();
return;
}
startPolling();
};
// If SSE connects but no payload arrives quickly (common through tunnels), fallback.
setTimeout(() => {
if (!receivedAnyMessage && state.transport === "streaming" && isTmuxRouteActive()) {
startPolling();
}
}, 3000);
}
function updateTransportForLifecycle() {
if (lifecycleVersion !== tmuxLifecycleVersion) {
stopLiveTransport();
if (lifecycleTimer) {
clearInterval(lifecycleTimer);
lifecycleTimer = null;
}
return;
}
if (!isTmuxRouteActive()) {
stopLiveTransport();
state.transport = "inactive";
if (lifecycleTimer) {
clearInterval(lifecycleTimer);
lifecycleTimer = null;
}
return;
}
if (document.visibilityState !== "visible") {
stopLiveTransport();
state.transport = "paused";
return;
}
if (isGalaxyTunnel()) {
startPolling();
return;
}
if (!liveEventSource && !livePollTimer) {
startEventStream();
}
}
updateTransportForLifecycle();
lifecycleTimer = setInterval(updateTransportForLifecycle, TRANSPORT_LIFECYCLE_INTERVAL_MS);
function getStatusLabel() {
if (state.transport === "polling") {
return "Tmux Live Log (Remote Mode)";
}
if (state.transport === "streaming") {
return "Tmux Live Log";
}
return "Tmux Live Log";
}
function togglePause() {
@@ -258,7 +378,7 @@ export function TmuxLog() {
<div class="tmux-block">
<div class="tmux-wrapper">
<div class="tmuxContainer">
<div class="tmuxHeader">Tmux Live Log</div>
<div class="tmuxHeader">${() => getStatusLabel()}</div>
<pre class="tmuxLog">${() => state.log}</pre>
</div>
</div>
@@ -88,9 +88,9 @@ export function ToggleControl() {
</section>
<section class="toggle-control-widget" style="margin-left: 1.5rem">
<div class="toggle-control-title">Reset Toggles to Default FrogPilot/Stock openpilot</div>
<div class="toggle-control-title">Reset Toggles to Default StarPilot</div>
<p class="toggle-control-text">
Reset all toggles to default FrogPilot/stock openpilot settings.
Reset all toggles to default StarPilot settings.
</p>
<button class="toggle-control-button" @click="${confirmResetDefault}">
Reset Toggles to Default
+26 -1
View File
@@ -3385,15 +3385,40 @@ def setup(app):
def generate():
last_output = ""
last_keepalive = 0.0
while True:
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
if output != last_output:
yield "data: " + "\n".join(reversed(output.splitlines())).replace("\n", "\ndata: ") + "\n\n"
last_output = output
last_keepalive = time.monotonic()
elif (time.monotonic() - last_keepalive) >= 5.0:
# Keep SSE alive through proxies/tunnels even when output is unchanged.
yield ": keepalive\n\n"
last_keepalive = time.monotonic()
time.sleep(0.5)
return Response(generate(), mimetype="text/event-stream")
response = Response(generate(), mimetype="text/event-stream")
response.headers["Cache-Control"] = "no-cache"
response.headers["X-Accel-Buffering"] = "no"
return response
@app.route("/api/tmux_log/snapshot", methods=["GET"])
def snapshot_tmux_log():
try:
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
except subprocess.CalledProcessError:
run_cmd(["tmux", "new-session", "-d", "-s", "comma", "-x", "240", "-y", "70", "bash"], "Started tmux session", "Failed to start tmux session")
output = subprocess.check_output(["tmux", "capture-pane", "-t", "comma:0", "-p", "-S", "-1000"], text=True)
except Exception as e:
return jsonify({"error": str(e)}), 500
try:
live_text = "\n".join(reversed(output.splitlines()))
return jsonify({"data": live_text}), 200
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/api/tmux_log/rename/<old>/<new>", methods=["PUT"])
def rename_tmux_log_path_params(old, new):