fix(GeneralPage): update section title from "General Settings" to "Preferences" for clarity

This commit is contained in:
Jaime Idolpx 2026-06-10 20:13:03 -04:00
parent 05be758754
commit 521fb8f613
3 changed files with 21 additions and 93 deletions

View File

@ -42,7 +42,7 @@ export default function GeneralPage({ config, setConfig }: GeneralPageProps) {
return (
<div className="p-4 space-y-4">
<h2 className="text-sm text-neutral-500 flex items-center gap-2"><Settings className="w-4 h-4" /> General Settings</h2>
<h2 className="text-sm text-neutral-500 flex items-center gap-2"><Settings className="w-4 h-4" /> Preferences</h2>
<div className="bg-white border border-neutral-200 rounded-lg divide-y divide-neutral-200">

View File

@ -1,21 +1,23 @@
import { useEffect, useRef, useState } from 'react';
import { useEffect, useRef } from 'react';
import { Terminal } from '@xterm/xterm';
import { FitAddon } from '@xterm/addon-fit';
import '@xterm/xterm/css/xterm.css';
import { ChevronLeft, Plug, Radio, RotateCcw, Unplug } from 'lucide-react';
import { ChevronLeft, Radio, RotateCcw } from 'lucide-react';
import { useWs } from '../ws';
interface Props { onBack: () => void; }
type ConnStatus = 'disconnected' | 'connecting' | 'connected' | 'error';
export default function SerialConsolePage({ onBack }: Props) {
const containerRef = useRef<HTMLDivElement>(null);
const termRef = useRef<Terminal | null>(null);
const fitRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [status, setStatus] = useState<ConnStatus>('disconnected');
const { status, send, subscribe } = useWs();
// Initialize xterm on mount
// Keep a stable ref to `send` so the xterm onData closure never goes stale
const sendRef = useRef(send);
useEffect(() => { sendRef.current = send; }, [send]);
// Initialize xterm once on mount
useEffect(() => {
const el = containerRef.current;
if (!el) return;
@ -42,12 +44,9 @@ export default function SerialConsolePage({ onBack }: Props) {
fitRef.current = fit;
term.writeln('\x1b[2m── Meatloaf Serial Console ──\x1b[0m');
term.writeln('\x1b[2mPress Connect to open a session.\x1b[0m');
term.writeln('');
term.onData((data) => {
if (wsRef.current?.readyState === WebSocket.OPEN) wsRef.current.send(data);
});
term.onData((data) => sendRef.current(data));
const onResize = () => fitRef.current?.fit();
window.addEventListener('resize', onResize);
@ -60,79 +59,20 @@ export default function SerialConsolePage({ onBack }: Props) {
};
}, []);
// Close WebSocket on unmount
// Forward all incoming WS messages to the terminal
useEffect(() => {
return () => {
if (wsRef.current) {
wsRef.current.onopen = wsRef.current.onmessage = wsRef.current.onerror = wsRef.current.onclose = null;
wsRef.current.close();
wsRef.current = null;
}
};
}, []);
const writeln = (msg: string) => termRef.current?.writeln(msg);
const connect = () => {
if (wsRef.current) {
wsRef.current.onopen = wsRef.current.onmessage = wsRef.current.onerror = wsRef.current.onclose = null;
wsRef.current.close();
wsRef.current = null;
}
const url = `ws://${window.location.hostname}/console`;
setStatus('connecting');
writeln(`\x1b[2mConnecting to ${url}\x1b[0m`);
const ws = new WebSocket(url);
ws.binaryType = 'arraybuffer';
wsRef.current = ws;
ws.onopen = () => {
setStatus('connected');
writeln('\x1b[32mConnected.\x1b[0m');
};
ws.onmessage = (ev) => {
if (!termRef.current) return;
if (ev.data instanceof ArrayBuffer) termRef.current.write(new Uint8Array(ev.data));
else termRef.current.write(String(ev.data));
};
let hadError = false;
ws.onerror = () => {
hadError = true;
writeln('\x1b[31mConnection error.\x1b[0m');
};
ws.onclose = (ev) => {
setStatus(hadError ? 'error' : 'disconnected');
wsRef.current = null;
if (!hadError) writeln(`\x1b[2mSession ended${ev.reason ? `: ${ev.reason}` : ''}.\x1b[0m`);
};
};
const disconnect = () => {
if (wsRef.current) {
wsRef.current.onopen = wsRef.current.onmessage = wsRef.current.onerror = wsRef.current.onclose = null;
wsRef.current.close();
wsRef.current = null;
}
setStatus('disconnected');
writeln('\x1b[2mDisconnected.\x1b[0m');
};
const busy = status === 'connecting' || status === 'connected';
return subscribe((msg) => {
termRef.current?.write(msg);
});
}, [subscribe]);
const statusColor =
status === 'connected' ? 'text-green-400' :
status === 'connecting' ? 'text-amber-400' :
status === 'error' ? 'text-red-400' : 'text-neutral-500';
status === 'connected' ? 'text-green-400' :
status === 'connecting' ? 'text-amber-400' : 'text-neutral-500';
const statusLabel =
status === 'connected' ? 'Connected' :
status === 'connecting' ? 'Connecting…' :
status === 'error' ? 'Error' : 'Disconnected';
status === 'connecting' ? 'Connecting…' : 'Disconnected';
return (
<div className="fixed inset-0 bg-[#0a0a0a] z-40 flex flex-col">
@ -143,7 +83,7 @@ export default function SerialConsolePage({ onBack }: Props) {
style={{ paddingTop: 'max(0.5rem, env(safe-area-inset-top))' }}
>
<button
onClick={() => { disconnect(); onBack(); }}
onClick={onBack}
className="p-1.5 rounded hover:bg-neutral-800 text-neutral-400 hover:text-white transition"
title="Back"
>
@ -165,18 +105,6 @@ export default function SerialConsolePage({ onBack }: Props) {
>
<RotateCcw className="w-4 h-4" />
</button>
<button
onClick={busy ? disconnect : connect}
className={`flex items-center gap-1.5 px-3 py-1 rounded text-xs font-medium transition ${
busy
? 'bg-red-900/60 hover:bg-red-800 text-red-300'
: 'bg-green-900/60 hover:bg-green-800 text-green-300'
}`}
>
{busy ? <Unplug className="w-3.5 h-3.5" /> : <Plug className="w-3.5 h-3.5" />}
<span>{busy ? 'Disconnect' : 'Connect'}</span>
</button>
</div>
{/* Terminal */}