feat(dashboard): default STT=medium, apply feedback+loading, log categories+filters

- Default STT model changed small -> medium (WhisperSTT default + service env;
  medium pre-cached). UI labels/hints updated.
- Model 적용 buttons: if the picked model == current, flash "변경사항 없음" for 3s
  then show the current model again; if it actually changes, STT shows a live
  loading panel that polls /api/models until the worker is ready and logs
  completion to the event log (set_stt_model/set_llm_model now return `changed`).
- Event/error log gains categories (READY, CONNECT, MODEL, TTS, VOICE, FILTER,
  SETTING, TURN, BRAIN, VISION, PIPELINE, …): Monitor.log takes an optional `cat`,
  call sites tagged, and each line shows a coloured category chip. A bot
  (re)connection now logs a CONNECT event.
- Log search extended: filter by 레벨(type), 종류(category, auto-populated), and
  시간(time range) in addition to free text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
EJClaw
2026-08-26 23:51:58 +09:00
parent d164630bb8
commit 1ac45214ce
5 changed files with 136 additions and 44 deletions

View File

@@ -129,7 +129,7 @@ class Turn:
# Guarded so the repeated _touch()/finish() calls can't double-count.
if self.status == "error" and not self._error_logged:
self._error_logged = True
self._monitor.log("error", f"대화 #{self.id} 실패: {self.error}")
self._monitor.log("error", f"대화 #{self.id} 실패: {self.error}", cat="TURN")
self._touch()
# -- internal --------------------------------------------------------- #
@@ -209,12 +209,17 @@ class Monitor:
self._status["claude_output_tokens"] += int(output_tokens or 0)
self._broadcast({"type": "status", "status": self.status_snapshot()})
def log(self, level: str, message: str) -> None:
"""A free-form lifecycle/error line (startup, disconnect, crash…)."""
def log(self, level: str, message: str, cat: str | None = None) -> None:
"""A free-form lifecycle/error line (startup, disconnect, crash…).
``cat`` is a short category tag (READY, CONNECT, MODEL, TTS, VOICE,
FILTER, SETTING, TURN, BRAIN, PIPELINE, …) shown as a chip and filterable
on the dashboard. Defaults to the upper-cased level when omitted."""
with self._lock:
self._event_id += 1
evt = {"type": "log", "id": self._event_id, "level": level,
"message": message, "wall": _now_wall()}
"cat": (cat or level.upper()), "message": message,
"wall": _now_wall()}
self._events.append(evt)
if level == "error":
self._status["errors_total"] += 1