mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-14 18:22:46 -06:00
feat(debug): live per-device debug logging toggle on the device screen
Checkbox on the device-detail page streams the Android player's player/zone logs live (no adb). Transient (off on reconnect), not persisted. - Android: DebugLog util (logcat + optional socket emit); 'set_debug' command wires the sink + flag; key player/zone decisions (layout mode, playItem, per-zone render) emit through it. - Server: relay device:log -> dashboard workspace room as dashboard:device-log. - Dashboard: 'Debug logging' checkbox sends set_debug; live log panel streams lines (rendered via textContent; capped at 500).
This commit is contained in:
parent
50d7dbe222
commit
73912d5f58
|
|
@ -229,6 +229,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}.sorted().joinToString("|")
|
||||
val changed = assignmentSig != zoneManager?.lastAssignmentSig
|
||||
|
||||
com.remotedisplay.player.util.DebugLog.i("Player", "Layout: MULTI-ZONE (${layoutZones.length()} zones, layout=$layoutId), ${assignments.length()} assignments")
|
||||
if (zoneManager?.hasZones() != true || layoutId != currentLayoutId) {
|
||||
Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)")
|
||||
handler.post {
|
||||
|
|
@ -252,6 +253,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
} else {
|
||||
// Single-zone mode - use PlaylistController (existing behavior)
|
||||
com.remotedisplay.player.util.DebugLog.i("Player", "Layout: SINGLE/FULLSCREEN (${layoutZones?.length() ?: 0} zones), ${assignments.length()} assignments")
|
||||
if (zoneManager?.hasZones() == true) handler.post { zoneManager?.cleanup() }
|
||||
playlistController.updatePlaylist(assignments)
|
||||
}
|
||||
|
|
@ -420,6 +422,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
private fun playItem(item: PlaylistItem) {
|
||||
hideStatus()
|
||||
com.remotedisplay.player.util.DebugLog.i("Player", "playItem: ${item.filename} mime=${item.mimeType} widget=${item.widgetId ?: "-"} zone=fullscreen")
|
||||
|
||||
// Widget content - render fullscreen in a WebView (single-zone / fullscreen
|
||||
// layouts; multi-zone widgets go through ZoneManager). Previously unhandled,
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ class ZoneManager(
|
|||
val widgetConfig = if (firstAssignment.isNull("widget_config")) null else firstAssignment.optString("widget_config", null)
|
||||
val contentId = if (firstAssignment.isNull("content_id")) null else firstAssignment.optString("content_id", null)
|
||||
val filepath = firstAssignment.optString("filepath", "")
|
||||
com.remotedisplay.player.util.DebugLog.i("Zone", "Zone '${zone.name}' (${zone.widthPercent.toInt()}x${zone.heightPercent.toInt()}%) -> ${widgetType?.let { "widget:$it" } ?: mimeType.ifEmpty { "empty" }}")
|
||||
val isMuted = firstAssignment.optInt("muted", 0) == 1
|
||||
|
||||
when {
|
||||
|
|
|
|||
|
|
@ -269,6 +269,21 @@ class WebSocketService : Service() {
|
|||
"screen_on" -> {
|
||||
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "224")).waitFor() } catch (_: Exception) {} }.start()
|
||||
}
|
||||
"set_debug" -> {
|
||||
val on = payload?.optBoolean("enabled", false) ?: false
|
||||
// Point the sink at this socket, then flip the flag. When on,
|
||||
// DebugLog.* mirrors player/zone lines to the dashboard.
|
||||
com.remotedisplay.player.util.DebugLog.sink = { tag, level, msg ->
|
||||
try {
|
||||
socket?.emit("device:log", JSONObject().apply {
|
||||
put("tag", tag); put("level", level); put("message", msg)
|
||||
})
|
||||
} catch (_: Throwable) {}
|
||||
}
|
||||
com.remotedisplay.player.util.DebugLog.enabled = on
|
||||
Log.i("WebSocketService", "Remote debug logging ${if (on) "ENABLED" else "disabled"}")
|
||||
com.remotedisplay.player.util.DebugLog.i("Debug", "Remote debug logging ${if (on) "ON" else "OFF"}")
|
||||
}
|
||||
else -> handler.post { try { onCommand?.invoke(type, payload) } catch (e: Throwable) { Log.e("WebSocketService", "onCommand cb: ${e.message}") } }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.remotedisplay.player.util
|
||||
|
||||
import android.util.Log
|
||||
|
||||
/**
|
||||
* Lightweight player debug logger. Always writes to logcat; when remote debug is
|
||||
* enabled (toggled from the dashboard device-detail screen via a `set_debug`
|
||||
* command), it ALSO streams the line to the server over the device socket so it
|
||||
* can be watched live without adb. Off by default; no network when disabled.
|
||||
*/
|
||||
object DebugLog {
|
||||
@Volatile var enabled = false
|
||||
// Set by WebSocketService: (tag, level, message) -> emit over the device socket.
|
||||
@Volatile var sink: ((String, String, String) -> Unit)? = null
|
||||
|
||||
fun d(tag: String, msg: String) { Log.d(tag, msg); send(tag, "d", msg) }
|
||||
fun i(tag: String, msg: String) { Log.i(tag, msg); send(tag, "i", msg) }
|
||||
fun w(tag: String, msg: String) { Log.w(tag, msg); send(tag, "w", msg) }
|
||||
fun e(tag: String, msg: String) { Log.e(tag, msg); send(tag, "e", msg) }
|
||||
|
||||
private fun send(tag: String, level: String, msg: String) {
|
||||
if (!enabled) return
|
||||
try { sink?.invoke(tag, level, msg) } catch (_: Throwable) {}
|
||||
}
|
||||
}
|
||||
|
|
@ -286,6 +286,8 @@ export default {
|
|||
'device.form.default_content_none': 'None (show "Waiting...")',
|
||||
'device.form.notes_label': 'Notes',
|
||||
'device.form.notes_placeholder': 'Location, setup details, etc.',
|
||||
'device.debug.toggle': 'Debug logging (live)',
|
||||
'device.debug.hint': 'Streams player/zone logs from this device in real time. Turns off on its own when the device reconnects.',
|
||||
'device.form.save_settings': 'Save Settings',
|
||||
// Control buttons
|
||||
'device.ctl.reboot_device': 'Reboot Device',
|
||||
|
|
|
|||
|
|
@ -52,6 +52,12 @@ export function connectSocket() {
|
|||
emit('playback-state', data);
|
||||
});
|
||||
|
||||
// Live device debug log line (device-detail screen streams these when the
|
||||
// per-device "Debug logging" checkbox is on).
|
||||
dashboardSocket.on('dashboard:device-log', (data) => {
|
||||
emit('device-log', data);
|
||||
});
|
||||
|
||||
// Playback progress (play_start with duration — drives device-card progress bars)
|
||||
dashboardSocket.on('dashboard:playback-progress', (data) => {
|
||||
emit('playback-progress', data);
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ let currentDevice = null;
|
|||
let statusHandler = null;
|
||||
let screenshotHandler = null;
|
||||
let playbackHandler = null;
|
||||
let logHandler = null;
|
||||
let screenshotInterval = null;
|
||||
let remoteActive = false;
|
||||
|
||||
|
|
@ -99,9 +100,24 @@ export function render(container, deviceId) {
|
|||
}
|
||||
};
|
||||
|
||||
// Live debug log lines streamed from the device (when the Debug logging
|
||||
// checkbox is on). Appended via textContent — no HTML injection.
|
||||
logHandler = (data) => {
|
||||
if (data.device_id !== deviceId) return;
|
||||
const panel = document.getElementById('debugLogPanel');
|
||||
if (!panel) return;
|
||||
const line = document.createElement('div');
|
||||
const time = new Date(data.ts || Date.now()).toLocaleTimeString();
|
||||
line.textContent = `${time} [${data.tag || ''}] ${data.message || ''}`;
|
||||
panel.appendChild(line);
|
||||
while (panel.childElementCount > 500) panel.removeChild(panel.firstChild);
|
||||
panel.scrollTop = panel.scrollHeight;
|
||||
};
|
||||
|
||||
on('device-status', statusHandler);
|
||||
on('screenshot-ready', screenshotHandler);
|
||||
on('playback-state', playbackHandler);
|
||||
on('device-log', logHandler);
|
||||
}
|
||||
|
||||
async function loadDevice(deviceId, activeTab = null) {
|
||||
|
|
@ -324,6 +340,15 @@ async function loadDevice(deviceId, activeTab = null) {
|
|||
</div>
|
||||
<button class="btn btn-secondary btn-sm" id="saveNotesBtn">${t('device.form.save_settings')}</button>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px">
|
||||
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;font-size:13px">
|
||||
<input type="checkbox" id="debugLogToggle"> ${t('device.debug.toggle')}
|
||||
</label>
|
||||
<div style="font-size:11px;color:var(--text-muted);margin:4px 0 0 24px">${t('device.debug.hint')}</div>
|
||||
<div id="debugLogPanel" style="display:none;margin-top:8px;background:#0b0f1a;border:1px solid var(--border);border-radius:6px;padding:8px;height:220px;overflow-y:auto;font-family:monospace;font-size:11px;line-height:1.45;color:#cbd5e1"></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">
|
||||
<button class="btn btn-secondary btn-sm" id="rebootBtn">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
|
|
@ -571,6 +596,15 @@ async function setupActions(device) {
|
|||
} catch {}
|
||||
|
||||
// Save settings (notes + orientation + default content)
|
||||
// Debug logging toggle: sends a transient set_debug command to the device and
|
||||
// reveals the live log panel. State is per-session (resets on device reconnect).
|
||||
document.getElementById('debugLogToggle')?.addEventListener('change', (e) => {
|
||||
const enabled = e.target.checked;
|
||||
const panel = document.getElementById('debugLogPanel');
|
||||
if (panel) panel.style.display = enabled ? 'block' : 'none';
|
||||
sendCommand(device.id, 'set_debug', { enabled });
|
||||
});
|
||||
|
||||
document.getElementById('saveNotesBtn')?.addEventListener('click', async () => {
|
||||
try {
|
||||
await api.updateDevice(device.id, {
|
||||
|
|
@ -1268,6 +1302,7 @@ export function cleanup() {
|
|||
if (statusHandler) off('device-status', statusHandler);
|
||||
if (screenshotHandler) off('screenshot-ready', screenshotHandler);
|
||||
if (playbackHandler) off('playback-state', playbackHandler);
|
||||
if (logHandler) off('device-log', logHandler);
|
||||
if (screenshotInterval) clearInterval(screenshotInterval);
|
||||
if (remoteActive && currentDevice) stopRemote(currentDevice.id);
|
||||
remoteActive = false;
|
||||
|
|
|
|||
|
|
@ -529,6 +529,22 @@ module.exports = function setupDeviceSocket(io) {
|
|||
emitToDeviceWorkspace(dashboardNs, currentDeviceId, 'dashboard:playback-state', data);
|
||||
});
|
||||
|
||||
// Live debug log line from the player (only sent when debug logging is toggled
|
||||
// on for this device). Relayed to the device's workspace dashboard room so the
|
||||
// open device-detail screen can stream it. Not persisted.
|
||||
socket.on('device:log', (data) => {
|
||||
if (!requireDeviceAuth() || !currentDeviceId) return;
|
||||
const message = typeof data?.message === 'string' ? data.message.slice(0, 2000) : '';
|
||||
if (!message) return;
|
||||
emitToDeviceWorkspace(dashboardNs, currentDeviceId, 'dashboard:device-log', {
|
||||
device_id: currentDeviceId,
|
||||
tag: typeof data?.tag === 'string' ? data.tag.slice(0, 64) : '',
|
||||
level: typeof data?.level === 'string' ? data.level.slice(0, 8) : 'd',
|
||||
message,
|
||||
ts: Date.now(),
|
||||
});
|
||||
});
|
||||
|
||||
// Play event logging (proof-of-play)
|
||||
socket.on('device:play-event', (data) => {
|
||||
if (!requireDeviceAuth()) return;
|
||||
|
|
|
|||
Loading…
Reference in a new issue