feat(monitoring): add server-side client logging and health infrastructure

- add Alembic migration c1d2e3f4g5h6 for client monitoring:
  - create client_logs table with FK to clients.uuid and performance indexes
  - extend clients with process/health tracking fields
- extend data model with ClientLog, LogLevel, ProcessStatus, and ScreenHealthStatus
- enhance listener MQTT handling:
  - subscribe to logs and health topics
  - persist client logs from infoscreen/{uuid}/logs/{level}
  - process health payloads and enrich heartbeat-derived client state
- add monitoring API blueprint server/routes/client_logs.py:
  - GET /api/client-logs/<uuid>/logs
  - GET /api/client-logs/summary
  - GET /api/client-logs/recent-errors
  - GET /api/client-logs/test
- register client_logs blueprint in server/wsgi.py
- align compose/dev runtime for listener live-code execution
- add client-side implementation docs:
  - CLIENT_MONITORING_SPECIFICATION.md
  - CLIENT_MONITORING_IMPLEMENTATION_GUIDE.md
- update TECH-CHANGELOG.md and copilot-instructions.md:
  - document monitoring changes
  - codify post-release technical-notes/no-version-bump convention
This commit is contained in:
2026-03-10 07:33:38 +00:00
parent 7746e26385
commit 3107d0f671
10 changed files with 2307 additions and 6 deletions

View File

@@ -21,6 +21,27 @@ class AcademicPeriodType(enum.Enum):
trimester = "trimester"
class LogLevel(enum.Enum):
ERROR = "ERROR"
WARN = "WARN"
INFO = "INFO"
DEBUG = "DEBUG"
class ProcessStatus(enum.Enum):
running = "running"
crashed = "crashed"
starting = "starting"
stopped = "stopped"
class ScreenHealthStatus(enum.Enum):
OK = "OK"
BLACK = "BLACK"
FROZEN = "FROZEN"
UNKNOWN = "UNKNOWN"
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True, autoincrement=True)
@@ -106,6 +127,31 @@ class Client(Base):
is_active = Column(Boolean, default=True, nullable=False)
group_id = Column(Integer, ForeignKey(
'client_groups.id'), nullable=False, default=1)
# Health monitoring fields
current_event_id = Column(Integer, nullable=True)
current_process = Column(String(50), nullable=True) # 'vlc', 'chromium', 'pdf_viewer'
process_status = Column(Enum(ProcessStatus), nullable=True)
process_pid = Column(Integer, nullable=True)
last_screenshot_analyzed = Column(TIMESTAMP(timezone=True), nullable=True)
screen_health_status = Column(Enum(ScreenHealthStatus), nullable=True, server_default='UNKNOWN')
last_screenshot_hash = Column(String(32), nullable=True)
class ClientLog(Base):
__tablename__ = 'client_logs'
id = Column(Integer, primary_key=True, autoincrement=True)
client_uuid = Column(String(36), ForeignKey('clients.uuid', ondelete='CASCADE'), nullable=False, index=True)
timestamp = Column(TIMESTAMP(timezone=True), nullable=False, index=True)
level = Column(Enum(LogLevel), nullable=False, index=True)
message = Column(Text, nullable=False)
context = Column(Text, nullable=True) # JSON stored as text
created_at = Column(TIMESTAMP(timezone=True), server_default=func.current_timestamp(), nullable=False)
__table_args__ = (
Index('ix_client_logs_client_timestamp', 'client_uuid', 'timestamp'),
Index('ix_client_logs_level_timestamp', 'level', 'timestamp'),
)
class EventType(enum.Enum):