feat: server-side PPTX conversion + screenshot dashboard system

PPTX Conversion:
- Remove LibreOffice from client dependencies (server uses Gotenberg)
- Update docs to reflect clients only display pre-rendered PDFs
- Clarify server-side conversion workflow in README and copilot-instructions

Screenshot System:
- Add background screenshot capture in display_manager.py
- Implement Wayland/X11 session detection with tool fallback
- Add client-side image processing (downscale + JPEG compression)
- Create timestamped files + latest.jpg symlink for easy access
- Implement file rotation (max 20 screenshots by default)
- Enhance simclient.py to transmit via MQTT dashboard topic
- Add structured JSON payload with screenshot, timestamp, system info
- New env vars: SCREENSHOT_CAPTURE_INTERVAL, SCREENSHOT_MAX_WIDTH,
  SCREENSHOT_JPEG_QUALITY, SCREENSHOT_MAX_FILES, SCREENSHOT_ALWAYS

Architecture: Two-process design with shared screenshots/ volume
This commit is contained in:
RobbStarkAustria
2025-11-30 13:49:27 +01:00
parent 65d7b99198
commit d021e21544
4 changed files with 623 additions and 100 deletions

View File

@@ -474,6 +474,22 @@ def get_latest_screenshot():
screenshot_dir = os.path.join(os.path.dirname(__file__), "screenshots")
if not os.path.exists(screenshot_dir):
return None
# Prefer 'latest.jpg' symlink/copy if present (written by display_manager)
preferred_path = os.path.join(screenshot_dir, "latest.jpg")
if os.path.exists(preferred_path):
try:
with open(preferred_path, "rb") as f:
screenshot_data = base64.b64encode(f.read()).decode('utf-8')
file_stats = os.stat(preferred_path)
logging.debug(f"Using preferred latest.jpg for screenshot ({file_stats.st_size} bytes)")
return {
"filename": os.path.basename(preferred_path),
"data": screenshot_data,
"timestamp": datetime.fromtimestamp(file_stats.st_mtime).isoformat(),
"size": file_stats.st_size
}
except Exception as e:
logging.debug(f"Could not read latest.jpg, falling back to newest file: {e}")
# Find the most recent screenshot file
screenshot_files = [f for f in os.listdir(screenshot_dir)
@@ -495,12 +511,14 @@ def get_latest_screenshot():
# Get file info
file_stats = os.stat(screenshot_path)
return {
info = {
"filename": latest_file,
"data": screenshot_data,
"timestamp": datetime.fromtimestamp(file_stats.st_mtime).isoformat(),
"size": file_stats.st_size
}
logging.debug(f"Selected latest screenshot: {latest_file} ({file_stats.st_size} bytes)")
return info
except Exception as e:
logging.error(f"Error reading screenshot: {e}")
@@ -526,12 +544,17 @@ def send_screenshot_heartbeat(client, client_id):
# Send to dashboard monitoring topic
dashboard_topic = f"infoscreen/{client_id}/dashboard"
client.publish(dashboard_topic, json.dumps(heartbeat_data))
if screenshot_info:
logging.info(f"Screenshot heartbeat sent: {screenshot_info['filename']} ({screenshot_info['size']} bytes)")
payload = json.dumps(heartbeat_data)
res = client.publish(dashboard_topic, payload, qos=0)
if res.rc == mqtt.MQTT_ERR_SUCCESS:
if screenshot_info:
logging.info(f"Dashboard heartbeat sent with screenshot: {screenshot_info['filename']} ({screenshot_info['size']} bytes)")
else:
logging.info("Dashboard heartbeat sent (no screenshot available)")
elif res.rc == mqtt.MQTT_ERR_NO_CONN:
logging.warning("Dashboard heartbeat publish returned NO_CONN; will retry on next interval")
else:
logging.debug("Heartbeat sent without screenshot")
logging.warning(f"Dashboard heartbeat publish failed with code: {res.rc}")
except Exception as e:
logging.error(f"Error sending screenshot heartbeat: {e}")