mirror of
https://github.com/screentinker/screentinker.git
synced 2026-06-21 21:52:33 -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("|")
|
}.sorted().joinToString("|")
|
||||||
val changed = assignmentSig != zoneManager?.lastAssignmentSig
|
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) {
|
if (zoneManager?.hasZones() != true || layoutId != currentLayoutId) {
|
||||||
Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)")
|
Log.i("MainActivity", "Multi-zone layout with ${layoutZones.length()} zones (layout=$layoutId, was=$currentLayoutId)")
|
||||||
handler.post {
|
handler.post {
|
||||||
|
|
@ -252,6 +253,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Single-zone mode - use PlaylistController (existing behavior)
|
// 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() }
|
if (zoneManager?.hasZones() == true) handler.post { zoneManager?.cleanup() }
|
||||||
playlistController.updatePlaylist(assignments)
|
playlistController.updatePlaylist(assignments)
|
||||||
}
|
}
|
||||||
|
|
@ -420,6 +422,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private fun playItem(item: PlaylistItem) {
|
private fun playItem(item: PlaylistItem) {
|
||||||
hideStatus()
|
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
|
// Widget content - render fullscreen in a WebView (single-zone / fullscreen
|
||||||
// layouts; multi-zone widgets go through ZoneManager). Previously unhandled,
|
// 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 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 contentId = if (firstAssignment.isNull("content_id")) null else firstAssignment.optString("content_id", null)
|
||||||
val filepath = firstAssignment.optString("filepath", "")
|
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
|
val isMuted = firstAssignment.optInt("muted", 0) == 1
|
||||||
|
|
||||||
when {
|
when {
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,21 @@ class WebSocketService : Service() {
|
||||||
"screen_on" -> {
|
"screen_on" -> {
|
||||||
Thread { try { Runtime.getRuntime().exec(arrayOf("input", "keyevent", "224")).waitFor() } catch (_: Exception) {} }.start()
|
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}") } }
|
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.default_content_none': 'None (show "Waiting...")',
|
||||||
'device.form.notes_label': 'Notes',
|
'device.form.notes_label': 'Notes',
|
||||||
'device.form.notes_placeholder': 'Location, setup details, etc.',
|
'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',
|
'device.form.save_settings': 'Save Settings',
|
||||||
// Control buttons
|
// Control buttons
|
||||||
'device.ctl.reboot_device': 'Reboot Device',
|
'device.ctl.reboot_device': 'Reboot Device',
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ export function connectSocket() {
|
||||||
emit('playback-state', data);
|
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)
|
// Playback progress (play_start with duration — drives device-card progress bars)
|
||||||
dashboardSocket.on('dashboard:playback-progress', (data) => {
|
dashboardSocket.on('dashboard:playback-progress', (data) => {
|
||||||
emit('playback-progress', data);
|
emit('playback-progress', data);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ let currentDevice = null;
|
||||||
let statusHandler = null;
|
let statusHandler = null;
|
||||||
let screenshotHandler = null;
|
let screenshotHandler = null;
|
||||||
let playbackHandler = null;
|
let playbackHandler = null;
|
||||||
|
let logHandler = null;
|
||||||
let screenshotInterval = null;
|
let screenshotInterval = null;
|
||||||
let remoteActive = false;
|
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('device-status', statusHandler);
|
||||||
on('screenshot-ready', screenshotHandler);
|
on('screenshot-ready', screenshotHandler);
|
||||||
on('playback-state', playbackHandler);
|
on('playback-state', playbackHandler);
|
||||||
|
on('device-log', logHandler);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadDevice(deviceId, activeTab = null) {
|
async function loadDevice(deviceId, activeTab = null) {
|
||||||
|
|
@ -324,6 +340,15 @@ async function loadDevice(deviceId, activeTab = null) {
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-secondary btn-sm" id="saveNotesBtn">${t('device.form.save_settings')}</button>
|
<button class="btn btn-secondary btn-sm" id="saveNotesBtn">${t('device.form.save_settings')}</button>
|
||||||
</div>
|
</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">
|
<div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">
|
||||||
<button class="btn btn-secondary btn-sm" id="rebootBtn">
|
<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">
|
<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 {}
|
} catch {}
|
||||||
|
|
||||||
// Save settings (notes + orientation + default content)
|
// 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 () => {
|
document.getElementById('saveNotesBtn')?.addEventListener('click', async () => {
|
||||||
try {
|
try {
|
||||||
await api.updateDevice(device.id, {
|
await api.updateDevice(device.id, {
|
||||||
|
|
@ -1268,6 +1302,7 @@ export function cleanup() {
|
||||||
if (statusHandler) off('device-status', statusHandler);
|
if (statusHandler) off('device-status', statusHandler);
|
||||||
if (screenshotHandler) off('screenshot-ready', screenshotHandler);
|
if (screenshotHandler) off('screenshot-ready', screenshotHandler);
|
||||||
if (playbackHandler) off('playback-state', playbackHandler);
|
if (playbackHandler) off('playback-state', playbackHandler);
|
||||||
|
if (logHandler) off('device-log', logHandler);
|
||||||
if (screenshotInterval) clearInterval(screenshotInterval);
|
if (screenshotInterval) clearInterval(screenshotInterval);
|
||||||
if (remoteActive && currentDevice) stopRemote(currentDevice.id);
|
if (remoteActive && currentDevice) stopRemote(currentDevice.id);
|
||||||
remoteActive = false;
|
remoteActive = false;
|
||||||
|
|
|
||||||
|
|
@ -529,6 +529,22 @@ module.exports = function setupDeviceSocket(io) {
|
||||||
emitToDeviceWorkspace(dashboardNs, currentDeviceId, 'dashboard:playback-state', data);
|
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)
|
// Play event logging (proof-of-play)
|
||||||
socket.on('device:play-event', (data) => {
|
socket.on('device:play-event', (data) => {
|
||||||
if (!requireDeviceAuth()) return;
|
if (!requireDeviceAuth()) return;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue