feat: story debug panel, log level filters, verbose story event logging
This commit is contained in:
@@ -33,6 +33,10 @@ class CompanionServer : Feature("CompanionServer") {
|
||||
private val capturedStories = java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.CopyOnWriteArrayList<Map<String, Any>>>()
|
||||
private val capturedStoryUrls = java.util.concurrent.ConcurrentHashMap<String, java.util.concurrent.ConcurrentHashMap<String, Boolean>>()
|
||||
|
||||
// Ring buffer of story debug events (last 100) — exposed at /story-debug
|
||||
private val storyDebugLog = ArrayDeque<Map<String, Any>>(100)
|
||||
private val storyDebugLock = Any()
|
||||
|
||||
// Prevent re-entrant log capture while the server itself logs
|
||||
private val isCapturing = ThreadLocal<Boolean>()
|
||||
|
||||
@@ -200,15 +204,34 @@ class CompanionServer : Feature("CompanionServer") {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun pushStoryDebug(type: String, extra: Map<String, Any> = emptyMap()) {
|
||||
val entry = mutableMapOf<String, Any>("ts" to System.currentTimeMillis().toString(), "type" to type)
|
||||
entry.putAll(extra)
|
||||
synchronized(storyDebugLock) {
|
||||
if (storyDebugLog.size >= 100) storyDebugLog.removeFirst()
|
||||
storyDebugLog.addLast(entry)
|
||||
}
|
||||
}
|
||||
|
||||
private fun subscribeToStoryEvents() {
|
||||
context.event.subscribe(NetworkApiRequestEvent::class) { event ->
|
||||
val urlPath = event.url.substringBefore("?")
|
||||
if (!urlPath.contains("df-mixer-prod")) return@subscribe
|
||||
val endpoint = urlPath.substringAfterLast("/")
|
||||
// Log every df-mixer-prod URL hit for debugging
|
||||
pushStoryDebug("seen", mapOf("url" to urlPath, "endpoint" to endpoint))
|
||||
context.log.info("CompanionServer: [story] seen url=$urlPath", "CompanionServer")
|
||||
if (endpoint !in setOf("stories", "batch_stories", "story_lookup", "batch_story_lookup")) return@subscribe
|
||||
pushStoryDebug("matched", mapOf("endpoint" to endpoint))
|
||||
context.log.info("CompanionServer: [$endpoint] matched, awaiting response", "CompanionServer")
|
||||
event.onSuccess { buffer ->
|
||||
buffer ?: return@onSuccess
|
||||
context.log.verbose("CompanionServer: [$endpoint] response ${buffer.size} bytes", "CompanionServer")
|
||||
if (buffer == null) {
|
||||
pushStoryDebug("null_buffer", mapOf("endpoint" to endpoint))
|
||||
context.log.warn("CompanionServer: [$endpoint] null buffer in onSuccess", "CompanionServer")
|
||||
return@onSuccess
|
||||
}
|
||||
context.log.info("CompanionServer: [$endpoint] response ${buffer.size} bytes", "CompanionServer")
|
||||
pushStoryDebug("response", mapOf("endpoint" to endpoint, "bytes" to buffer.size))
|
||||
runCatching {
|
||||
var parsed = 0
|
||||
// story_lookup / batch_story_lookup: field 1 → repeated story entries
|
||||
@@ -248,9 +271,11 @@ class CompanionServer : Feature("CompanionServer") {
|
||||
}
|
||||
}
|
||||
}
|
||||
context.log.verbose("CompanionServer: [$endpoint] parsed $parsed new stories", "CompanionServer")
|
||||
context.log.info("CompanionServer: [$endpoint] parsed $parsed new stories (total=${capturedStories.values.sumOf { it.size }})", "CompanionServer")
|
||||
pushStoryDebug("parsed", mapOf("endpoint" to endpoint, "count" to parsed, "totalStories" to capturedStories.values.sumOf { it.size }))
|
||||
}.onFailure {
|
||||
context.log.warn("CompanionServer: [$endpoint] proto parse failed: ${it.message}", "CompanionServer")
|
||||
pushStoryDebug("parse_error", mapOf("endpoint" to endpoint, "error" to (it.message ?: "unknown")))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,6 +384,7 @@ class CompanionServer : Feature("CompanionServer") {
|
||||
}
|
||||
serveJson(socket, result)
|
||||
}
|
||||
rawPath == "/story-debug" -> serveJson(socket, synchronized(storyDebugLock) { storyDebugLog.toList() })
|
||||
else -> respond404(socket)
|
||||
}
|
||||
}
|
||||
@@ -537,7 +563,7 @@ button:hover{background:#333}
|
||||
<style>
|
||||
*{box-sizing:border-box;margin:0;padding:0}
|
||||
body{font-family:monospace;background:#0d0d0d;color:#d0d0d0;height:100vh;display:flex;flex-direction:column;overflow:hidden}
|
||||
#bar{padding:6px 10px;background:#111;border-bottom:1px solid #222;display:flex;align-items:center;gap:14px;flex-shrink:0}
|
||||
#bar{padding:6px 10px;background:#111;border-bottom:1px solid #222;display:flex;align-items:center;gap:8px;flex-shrink:0;flex-wrap:wrap}
|
||||
#bar h1{font-size:13px;color:#bbb;font-weight:normal}
|
||||
#dot{font-size:11px}
|
||||
#dot.on{color:#4caf50}
|
||||
@@ -545,6 +571,8 @@ body{font-family:monospace;background:#0d0d0d;color:#d0d0d0;height:100vh;display
|
||||
#counts{font-size:10px;color:#555;margin-left:auto}
|
||||
a#logout{font-size:10px;padding:2px 8px;background:#181818;border:1px solid #333;color:#888;cursor:pointer;border-radius:2px;text-decoration:none}
|
||||
a#logout:hover{background:#222}
|
||||
.view-btn{font-size:10px;padding:2px 8px;background:#181818;border:1px solid #333;color:#888;cursor:pointer;border-radius:2px}.view-btn.active{background:#222;color:#ccc;border-color:#555}
|
||||
.view-btn:hover{background:#222}
|
||||
#main{display:flex;flex:1;overflow:hidden;min-height:0}
|
||||
.pane{flex:1;display:flex;flex-direction:column;border-right:1px solid #1a1a1a;overflow:hidden}
|
||||
.pane:last-child{border-right:none}
|
||||
@@ -559,7 +587,12 @@ a#logout:hover{background:#222}
|
||||
/* Log rows */
|
||||
.log-row{padding:0 6px;white-space:pre-wrap;word-break:break-all}
|
||||
.log-row:hover{background:#141414}
|
||||
.V{color:#555}.D{color:#5c8fbd}.I{color:#bbb}.W{color:#c9944a}.E{color:#c45252}.A{color:#c45252;font-weight:bold}
|
||||
.V{color:#777}.D{color:#5c8fbd}.I{color:#bbb}.W{color:#c9944a}.E{color:#c45252}.A{color:#c45252;font-weight:bold}
|
||||
/* Level filter hiding */
|
||||
#le.hide-V .V{display:none}#le.hide-D .D{display:none}#le.hide-I .I{display:none}#le.hide-W .W{display:none}#le.hide-E .E{display:none}
|
||||
.lv-btn{font-size:9px;padding:1px 5px;background:#181818;border:1px solid #333;cursor:pointer;border-radius:2px;font-weight:bold}
|
||||
.lv-btn:hover{background:#222}.lv-btn.on{border-color:#555}
|
||||
.lv-V{color:#777}.lv-D{color:#5c8fbd}.lv-I{color:#bbb}.lv-W{color:#c9944a}.lv-E{color:#c45252}
|
||||
/* Network rows */
|
||||
.net-row{display:flex;align-items:baseline;padding:0 6px;gap:6px;cursor:pointer;white-space:nowrap;overflow:hidden}
|
||||
.net-row:hover{background:#141414}
|
||||
@@ -574,6 +607,10 @@ a#logout:hover{background:#222}
|
||||
.status-pend{color:#555;font-size:10px;flex-shrink:0}
|
||||
.net-uri{font-size:10px;color:#888;overflow:hidden;text-overflow:ellipsis;flex:1}
|
||||
.net-size{font-size:9px;color:#444;flex-shrink:0}
|
||||
/* Stories panel */
|
||||
#stories-panel{flex:1;display:none;flex-direction:column;overflow:hidden}
|
||||
.sd-row{padding:2px 6px;white-space:pre-wrap;word-break:break-all}.sd-row:hover{background:#141414}
|
||||
.sd-seen{color:#555}.sd-matched{color:#5c8fbd}.sd-response{color:#4caf50}.sd-parsed{color:#c9944a}.sd-null_buffer{color:#c45252}.sd-parse_error{color:#c45252}
|
||||
/* Detail panel */
|
||||
#detail{flex-direction:column;flex-shrink:0;height:45%;border-top:2px solid #2a2a2a;background:#0a0a0a;overflow:hidden}
|
||||
#detail-bar{padding:4px 8px;background:#111;border-bottom:1px solid #222;display:flex;align-items:center;gap:6px;flex-wrap:wrap;flex-shrink:0}
|
||||
@@ -593,6 +630,8 @@ a#logout:hover{background:#222}
|
||||
<div id="bar">
|
||||
<h1>PurrfectSnap Companion</h1>
|
||||
<span id="dot" class="off">● disconnected</span>
|
||||
<button class="view-btn active" id="vbtn-main" onclick="switchView('main')">Main</button>
|
||||
<button class="view-btn" id="vbtn-stories" onclick="switchView('stories')">Stories <span id="story-count"></span></button>
|
||||
<span id="counts"></span>
|
||||
<a id="logout" href="/logout">Logout</a>
|
||||
</div>
|
||||
@@ -601,6 +640,11 @@ a#logout:hover{background:#222}
|
||||
<div class="pane-bar">
|
||||
<span class="title">Logs</span>
|
||||
<input id="lf" type="text" placeholder="filter…" oninput="filter('le','lf')"/>
|
||||
<button class="lv-btn lv-V on" id="lvbtn-V" onclick="toggleLevel('V')">V</button>
|
||||
<button class="lv-btn lv-D on" id="lvbtn-D" onclick="toggleLevel('D')">D</button>
|
||||
<button class="lv-btn lv-I on" id="lvbtn-I" onclick="toggleLevel('I')">I</button>
|
||||
<button class="lv-btn lv-W on" id="lvbtn-W" onclick="toggleLevel('W')">W</button>
|
||||
<button class="lv-btn lv-E on" id="lvbtn-E" onclick="toggleLevel('E')">E</button>
|
||||
<button onclick="clr('le')">Clear</button>
|
||||
<label><input type="checkbox" id="ls" checked> scroll</label>
|
||||
</div>
|
||||
@@ -616,6 +660,15 @@ a#logout:hover{background:#222}
|
||||
<div class="entries" id="ne"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="stories-panel">
|
||||
<div class="pane-bar">
|
||||
<span class="title">Story Debug</span>
|
||||
<button onclick="loadStoryDebug()">Refresh</button>
|
||||
<button onclick="clr('sd-log')">Clear</button>
|
||||
<span id="sd-stats" style="font-size:10px;color:#555;margin-left:8px"></span>
|
||||
</div>
|
||||
<div class="entries" id="sd-log"></div>
|
||||
</div>
|
||||
<div id="detail" style="display:none">
|
||||
<div id="detail-bar">
|
||||
<span id="d-badge" class="badge"></span>
|
||||
@@ -631,8 +684,18 @@ a#logout:hover{background:#222}
|
||||
</div>
|
||||
<script>
|
||||
var lc=0,nc=0,netMap={},detailId=null,detailTab='res',hexMode=false,detailFull=null;
|
||||
var sdTimer=null;
|
||||
function ts(e){return new Date(+e).toISOString().slice(11,23)}
|
||||
function q(id){return document.getElementById(id)}
|
||||
// ---- View switching ----
|
||||
function switchView(v){
|
||||
q('main').style.display=v==='main'?'flex':'none';
|
||||
q('stories-panel').style.display=v==='stories'?'flex':'none';
|
||||
q('vbtn-main').className='view-btn'+(v==='main'?' active':'');
|
||||
q('vbtn-stories').className='view-btn'+(v==='stories'?' active':'');
|
||||
if(v==='stories'){loadStoryDebug();if(!sdTimer)sdTimer=setInterval(loadStoryDebug,5000);}
|
||||
else{if(sdTimer){clearInterval(sdTimer);sdTimer=null;}}
|
||||
}
|
||||
function loadHistory(){
|
||||
fetch('/logs').then(function(r){return r.json();}).then(function(a){a.forEach(function(e){addLog(e,false);});});
|
||||
fetch('/network').then(function(r){return r.json();}).then(function(a){a.forEach(function(e){addNet(e,false);});});
|
||||
@@ -645,6 +708,12 @@ function startSSE(){
|
||||
es.addEventListener('network',function(e){addNet(JSON.parse(e.data),true);});
|
||||
es.addEventListener('network_update',function(e){updateNet(JSON.parse(e.data));});
|
||||
}
|
||||
// ---- Log level filter ----
|
||||
function toggleLevel(lv){
|
||||
var le=q('le'),cl=le.classList,btn=q('lvbtn-'+lv);
|
||||
if(cl.contains('hide-'+lv)){cl.remove('hide-'+lv);btn.classList.add('on');}
|
||||
else{cl.add('hide-'+lv);btn.classList.remove('on');}
|
||||
}
|
||||
// ---- Logs ----
|
||||
function addLog(e,live){
|
||||
lc++;
|
||||
@@ -810,8 +879,35 @@ function closeDetail(){
|
||||
detailFull=null;
|
||||
q('detail').style.display='none';
|
||||
q('ne').querySelectorAll('.net-row').forEach(function(r){r.classList.remove('selected');});
|
||||
}// ---- Stories debug ----
|
||||
function loadStoryDebug(){
|
||||
fetch('/story-debug').then(function(r){return r.json();}).then(function(a){
|
||||
clr('sd-log');
|
||||
a.forEach(function(e){addStoryDebugRow(e);});
|
||||
var last=q('sd-log').lastChild;
|
||||
if(last)last.scrollIntoView();
|
||||
fetch('/stories').then(function(r){return r.json();}).then(function(s){
|
||||
var total=0,users=0;
|
||||
if(Array.isArray(s)){users=s.length;s.forEach(function(g){total+=(g.stories||[]).length;});}
|
||||
q('story-count').textContent=total?'('+total+')':(users?'(0)':'');
|
||||
q('sd-stats').textContent='Users: '+users+' | Stories: '+total;
|
||||
}).catch(function(){});
|
||||
}).catch(function(){if(q('sd-stats'))q('sd-stats').textContent='Error loading /story-debug';});
|
||||
}
|
||||
// ---- Shared ----
|
||||
function addStoryDebugRow(e){
|
||||
var el=document.createElement('div');
|
||||
var t=e.type||'?';
|
||||
el.className='sd-row sd-'+t;
|
||||
var extra='';
|
||||
if(t==='seen')extra=' url='+e.url;
|
||||
else if(t==='matched')extra=' endpoint='+e.endpoint;
|
||||
else if(t==='response')extra=' endpoint='+e.endpoint+' bytes='+e.bytes;
|
||||
else if(t==='parsed')extra=' endpoint='+e.endpoint+' new='+e.count+' total='+e.totalStories;
|
||||
else if(t==='null_buffer')extra=' endpoint='+e.endpoint+' (null buffer!)';
|
||||
else if(t==='parse_error')extra=' endpoint='+e.endpoint+' ERR: '+e.error;
|
||||
el.textContent='['+ts(e.ts||'0')+'] '+t.toUpperCase()+extra;
|
||||
q('sd-log').appendChild(el);
|
||||
}// ---- Shared ----
|
||||
function filter(listId,filterId){
|
||||
var f=q(filterId).value.toLowerCase();
|
||||
q(listId).querySelectorAll('.log-row').forEach(function(el){
|
||||
|
||||
Reference in New Issue
Block a user