feat(dashboard): VSCode-style log dock, 3-row turns, log search/edit/delete
- Bottom-docked collapsible terminal log panel (open/close), retains the event
log from voice-server start (event tail 200→2000).
- Log search box + level filter (전체/오류/경고/정보); per-line 삭제/수정 and
전체 삭제, backed by new monitor event ids and /api/logs/{clear,delete,edit}.
- Turns now show 들음 / 생각 / 답변 three rows; 생각 surfaces the emotion-tone
plan the bot chose (and the [잡음] decision), via a new Turn.thought field.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -83,6 +83,7 @@ class Turn:
|
||||
self.wall = _now_wall()
|
||||
self._t0 = _now_mono()
|
||||
self.heard_text = ""
|
||||
self.thought_text = ""
|
||||
self.reply_text = ""
|
||||
self.status = "active" # active | ok | error
|
||||
self.error = ""
|
||||
@@ -95,6 +96,10 @@ class Turn:
|
||||
self.heard_text = text
|
||||
self._touch()
|
||||
|
||||
def thought(self, text: str) -> None:
|
||||
self.thought_text = text
|
||||
self._touch()
|
||||
|
||||
def replied(self, text: str) -> None:
|
||||
self.reply_text = text
|
||||
self._touch()
|
||||
@@ -134,6 +139,7 @@ class Turn:
|
||||
"source": self.source,
|
||||
"wall": self.wall,
|
||||
"heard": self.heard_text,
|
||||
"thought": self.thought_text,
|
||||
"reply": self.reply_text,
|
||||
"status": self.status,
|
||||
"error": self.error,
|
||||
@@ -147,7 +153,9 @@ class Monitor:
|
||||
|
||||
def __init__(self, keep: int = 60) -> None:
|
||||
self._turns: deque[Turn] = deque(maxlen=keep)
|
||||
self._events: deque[dict[str, Any]] = deque(maxlen=200)
|
||||
# Keep a long event tail so the terminal panel can show the log from
|
||||
# (voice-server) start, not just the last few lines.
|
||||
self._events: deque[dict[str, Any]] = deque(maxlen=2000)
|
||||
self._status: dict[str, Any] = {
|
||||
"running": False,
|
||||
"listening": False,
|
||||
@@ -159,6 +167,7 @@ class Monitor:
|
||||
self._lock = threading.Lock()
|
||||
self._subs: list["queue.Queue[str]"] = []
|
||||
self._id = 0
|
||||
self._event_id = 0
|
||||
|
||||
# -- status ----------------------------------------------------------- #
|
||||
def set_status(self, **kw: Any) -> None:
|
||||
@@ -179,13 +188,48 @@ class Monitor:
|
||||
|
||||
def log(self, level: str, message: str) -> None:
|
||||
"""A free-form lifecycle/error line (startup, disconnect, crash…)."""
|
||||
evt = {"type": "log", "level": level, "message": message, "wall": _now_wall()}
|
||||
with self._lock:
|
||||
self._event_id += 1
|
||||
evt = {"type": "log", "id": self._event_id, "level": level,
|
||||
"message": message, "wall": _now_wall()}
|
||||
self._events.append(evt)
|
||||
if level == "error":
|
||||
self._status["errors_total"] += 1
|
||||
self._broadcast(evt)
|
||||
|
||||
def delete_event(self, event_id: int) -> bool:
|
||||
"""Remove one log line by id (dashboard per-line '삭제')."""
|
||||
found = False
|
||||
with self._lock: # _broadcast re-locks, so must run outside this block
|
||||
for e in list(self._events):
|
||||
if e.get("id") == event_id:
|
||||
self._events.remove(e)
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
self._broadcast({"type": "log_deleted", "id": event_id})
|
||||
return found
|
||||
|
||||
def edit_event(self, event_id: int, message: str) -> bool:
|
||||
"""Edit one log line's text by id (dashboard per-line '수정')."""
|
||||
found = False
|
||||
with self._lock: # _broadcast re-locks, so must run outside this block
|
||||
for e in self._events:
|
||||
if e.get("id") == event_id:
|
||||
e["message"] = message
|
||||
found = True
|
||||
break
|
||||
if found:
|
||||
self._broadcast({"type": "log_edited", "id": event_id, "message": message})
|
||||
return found
|
||||
|
||||
def clear_events(self) -> None:
|
||||
"""Wipe the event/error log (dashboard '로그 삭제'). Broadcasts a reset so
|
||||
every connected page clears its terminal panel too."""
|
||||
with self._lock:
|
||||
self._events.clear()
|
||||
self._broadcast({"type": "logs_cleared"})
|
||||
|
||||
# -- turns ------------------------------------------------------------ #
|
||||
def turn(self, source: str = "voice") -> Turn:
|
||||
with self._lock:
|
||||
|
||||
Reference in New Issue
Block a user