mirror of
https://github.com/DarkflameUniverse/DarkflameServer.git
synced 2026-10-02 02:43:44 +00:00
The Packet Captures page (/inspector/packets, dev_message_inspector) arms captures with a search picker (part of an account or character name, or a pasted account or object ID; online ones marked; everything needs no pick), lists running and saved ones live (packet_capture topic), and plays one back on its timeline: play, pause, seek and speed, packets appearing in order with their decoded fields and bytes. World 3D takes ?capture=<id>&zone=<zone> and replays the captured position updates in its replay mode; the capture page's playhead drives it over a BroadcastChannel. Saved packet captures show in the Message Inspector's list and open there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
233 lines
15 KiB
Django/Jinja
233 lines
15 KiB
Django/Jinja
{% extends "base.jinja2" %}
|
|
|
|
{% block title %}Message Inspector - DarkflameServer{% endblock %}
|
|
|
|
{% block css %}
|
|
<style>
|
|
.inspector-layout { display: grid; grid-template-columns: 17rem minmax(0, 1fr); gap: 1rem; align-items: start; }
|
|
.inspector-side { position: sticky; top: 4rem; }
|
|
/* The viewer takes the whole width; the side (new capture, running ones) is on the Saved captures tab */
|
|
.inspector-layout.viewing { grid-template-columns: minmax(0, 1fr); }
|
|
.inspector-layout.viewing .inspector-side { display: none; }
|
|
.inspector-side .list-group-item { cursor: pointer; }
|
|
.inspector-side .list-group-item.active .text-body-secondary { color: inherit !important; opacity: .85; }
|
|
.sessions-table th[data-sort] { cursor: pointer; white-space: nowrap; user-select: none; }
|
|
.sessions-table th[data-sort]::after { content: ''; display: inline-block; width: 1em; }
|
|
.sessions-table th[data-sort].sorted-asc::after { content: '\25B4'; }
|
|
.sessions-table th[data-sort].sorted-desc::after { content: '\25BE'; }
|
|
.sessions-table tbody tr { cursor: pointer; }
|
|
.sessions-table td { vertical-align: middle; }
|
|
.viewer-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(24rem, 30rem); gap: 1rem; align-items: start; }
|
|
.viewer-grid.no-detail { grid-template-columns: minmax(0, 1fr); }
|
|
.msg-table-wrap { height: calc(100vh - 22rem); min-height: 18rem; overflow: auto; position: relative; }
|
|
.msg-table { font-size: .8rem; table-layout: fixed; width: 100%; }
|
|
.msg-table td, .msg-table th { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.msg-table tbody tr { height: 26px; cursor: pointer; }
|
|
.msg-table tbody td { padding-top: 0; padding-bottom: 0; line-height: 25px; }
|
|
.msg-table tbody tr.spacer { cursor: default; }
|
|
.msg-table tbody tr.spacer td { padding: 0; border: 0; }
|
|
.msg-table tbody tr.selected > td { background: var(--bs-primary-bg-subtle); }
|
|
.msg-table tbody tr.has-gap > td:first-child { box-shadow: inset 3px 0 0 var(--bs-warning); }
|
|
.msg-table tbody tr.zone-change > td { border-top: 2px solid var(--bs-info); }
|
|
.msg-table .col-seq { width: 5rem; }
|
|
.msg-table .col-time { width: 7.5rem; }
|
|
.msg-table .col-dir { width: 5.5rem; }
|
|
.msg-table .col-msg { width: 16rem; }
|
|
.msg-table .col-obj { width: 11rem; }
|
|
.msg-table .col-bytes { width: 4rem; }
|
|
.type-table-wrap { max-height: 14rem; overflow: auto; }
|
|
.type-table { font-size: .8rem; }
|
|
.type-table tbody tr { cursor: pointer; }
|
|
.type-table tbody tr.active-filter > td { background: var(--bs-primary-bg-subtle); }
|
|
.detail-card { position: sticky; top: 4rem; max-height: calc(100vh - 5rem); overflow: auto; }
|
|
.detail-fields { font-size: .8rem; }
|
|
.detail-fields th { width: 35%; word-break: break-all; }
|
|
.hexdump { font-family: var(--bs-font-monospace); font-size: .75rem; white-space: pre; overflow-x: auto; margin: 0; line-height: 1.45; }
|
|
.hexdump .off { color: var(--bs-secondary-color); }
|
|
.hexdump .b.hl, .hexdump .a.hl { background: var(--bs-warning-bg-subtle); outline: 1px solid var(--bs-warning); }
|
|
/* Narrower: the message goes under the table, which gets shorter so both fit */
|
|
@media (max-width: 1799.98px) {
|
|
.viewer-grid { grid-template-columns: minmax(0, 1fr); }
|
|
.viewer-grid:not(.no-detail) .msg-table-wrap { height: 42vh; }
|
|
.detail-card { position: static; max-height: none; }
|
|
}
|
|
@media (max-width: 991.98px) { .inspector-layout { grid-template-columns: minmax(0, 1fr); } .inspector-side { position: static; } }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div id="inspectorPage">
|
|
<div class="mb-3">
|
|
<h2 class="mb-1">Game Message Inspector <a class="btn btn-sm btn-outline-primary align-middle ms-2" href="/inspector/packets">Packet captures</a></h2>
|
|
<p class="text-body-secondary mb-0">Capture the game messages one online player sends and receives, live, to debug scripts and behaviors, and look
|
|
at them again later. The world server the player is in captures them; messages the server has a typed struct for show their fields, all
|
|
show their bytes. A capture follows the player to other zones and always stops at its time limit. Every capture is saved (deleted after
|
|
the retention set under Settings, Data retention). Starting, stopping, opening someone else's saved capture, downloading and deleting are audited.</p>
|
|
</div>
|
|
|
|
<div class="inspector-layout" id="inspectorLayout">
|
|
<aside class="inspector-side">
|
|
<div class="card mb-3">
|
|
<div class="card-header"><h5 class="mb-0 fs-6">New capture</h5></div>
|
|
<div class="card-body">
|
|
<form id="startForm" class="d-grid gap-2">
|
|
<div><label class="form-label small mb-1" for="captureCharacter">Character</label>
|
|
<input class="form-control form-control-sm" id="captureCharacter" required placeholder="Name or ID" autocomplete="off"></div>
|
|
<div><label class="form-label small mb-1" for="captureSeconds">For</label>
|
|
<select class="form-select form-select-sm" id="captureSeconds" data-pref="inspector.seconds">
|
|
<option value="30">30 seconds</option>
|
|
<option value="60">1 minute</option>
|
|
<option value="120" selected>2 minutes</option>
|
|
<option value="300">5 minutes</option>
|
|
<option value="600">10 minutes</option>
|
|
<option value="900">15 minutes</option>
|
|
</select></div>
|
|
<div class="d-flex gap-3 small">
|
|
<div class="form-check mb-0"><input class="form-check-input" type="checkbox" id="captureToServer" checked><label class="form-check-label" for="captureToServer">Sent by the client</label></div>
|
|
<div class="form-check mb-0"><input class="form-check-input" type="checkbox" id="captureToClient" checked><label class="form-check-label" for="captureToClient">Sent to it</label></div>
|
|
</div>
|
|
<div><label class="form-label small mb-1" for="captureOnly">Only these messages</label>
|
|
<input class="form-control form-control-sm" id="captureOnly" list="messageNames" placeholder="e.g. REQUEST_USE, 154 (empty: all)"></div>
|
|
<div><label class="form-label small mb-1" for="captureSkip">Never these</label>
|
|
<input class="form-control form-control-sm" id="captureSkip" list="messageNames" data-pref="inspector.skip" placeholder="e.g. READY_FOR_UPDATES"></div>
|
|
<button class="btn btn-sm btn-primary">Start capture</button>
|
|
</form>
|
|
<datalist id="messageNames"></datalist>
|
|
</div>
|
|
</div>
|
|
<div class="card">
|
|
<div class="card-header"><h5 class="mb-0 fs-6">Running and recent</h5></div>
|
|
<div class="list-group list-group-flush" id="captureList"><div class="list-group-item text-body-secondary small">Loading…</div></div>
|
|
</div>
|
|
</aside>
|
|
|
|
<div style="min-width: 0">
|
|
<ul class="nav nav-tabs mb-3" role="tablist">
|
|
<li class="nav-item" role="presentation"><button class="nav-link active" id="sessionsTab" type="button" role="tab" aria-controls="sessionsPane" aria-selected="true">Saved captures</button></li>
|
|
<li class="nav-item" role="presentation"><button class="nav-link" id="viewerTab" type="button" role="tab" aria-controls="viewerPane" aria-selected="false" disabled>Viewer</button></li>
|
|
</ul>
|
|
|
|
<!-- Saved captures -->
|
|
<div id="sessionsPane" role="tabpanel" aria-labelledby="sessionsTab">
|
|
<form class="row g-2 align-items-end mb-2" id="sessionFilters">
|
|
<div class="col-sm-6 col-lg-3"><label class="form-label small mb-1" for="filterCharacter">Character</label>
|
|
<input class="form-control form-control-sm" id="filterCharacter" placeholder="Name or ID"></div>
|
|
<div class="col-sm-6 col-lg-2"><label class="form-label small mb-1" for="filterAccount">Account</label>
|
|
<input class="form-control form-control-sm" id="filterAccount" placeholder="Name or ID"></div>
|
|
<div class="col-sm-4 col-lg-2"><label class="form-label small mb-1" for="filterStaff">Started by</label>
|
|
<input class="form-control form-control-sm" id="filterStaff" placeholder="Staff account"></div>
|
|
<div class="col-sm-4 col-lg-2"><label class="form-label small mb-1" for="filterSince">From</label>
|
|
<input class="form-control form-control-sm" type="date" id="filterSince"></div>
|
|
<div class="col-sm-4 col-lg-2"><label class="form-label small mb-1" for="filterUntil">To</label>
|
|
<input class="form-control form-control-sm" type="date" id="filterUntil"></div>
|
|
<div class="col-lg-1 d-grid"><button type="button" class="btn btn-sm btn-outline-secondary" id="filterClear">Clear</button></div>
|
|
</form>
|
|
<div class="table-responsive border rounded">
|
|
<table class="table table-sm table-hover mb-0 sessions-table">
|
|
<thead><tr>
|
|
<th data-sort="started">Started</th>
|
|
<th data-sort="character">Character</th>
|
|
<th data-sort="started_by">By</th>
|
|
<th>Length</th>
|
|
<th>Where</th>
|
|
<th class="text-end" data-sort="messages">Messages</th>
|
|
<th class="text-end" data-sort="bytes">Size</th>
|
|
<th>Ended</th>
|
|
<th></th>
|
|
</tr></thead>
|
|
<tbody id="sessionRows"><tr><td colspan="9" class="text-body-secondary small">Loading…</td></tr></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-2 small">
|
|
<div class="text-body-secondary" id="sessionInfo"></div>
|
|
<div class="d-flex align-items-center gap-2">
|
|
<select class="form-select form-select-sm w-auto" id="sessionPageSize" data-pref="inspector.pageSize" aria-label="Captures per page">
|
|
<option value="25">25 per page</option><option value="50">50 per page</option><option value="100">100 per page</option>
|
|
</select>
|
|
<div class="btn-group btn-group-sm">
|
|
<button type="button" class="btn btn-outline-secondary" id="sessionPrev">Previous</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="sessionNext">Next</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- One capture -->
|
|
<div id="viewerPane" class="d-none" role="tabpanel" aria-labelledby="viewerTab">
|
|
<div class="card mb-3">
|
|
<div class="card-header d-flex flex-wrap justify-content-between align-items-center gap-2">
|
|
<div style="min-width: 0"><h5 class="mb-0" id="captureTitle"></h5><div class="small text-body-secondary" id="captureSubtitle"></div></div>
|
|
<div class="d-flex flex-wrap gap-2">
|
|
<button class="btn btn-sm btn-outline-secondary d-none" id="pauseBtn" title="Hold new messages back while you look (they are still captured)">Pause</button>
|
|
<a class="btn btn-sm btn-outline-secondary" id="downloadBtn" title="Every message with its fields and bytes (audited)">Download JSON</a>
|
|
<button class="btn btn-sm btn-outline-danger d-none" id="stopBtn">Stop</button>
|
|
<button class="btn btn-sm btn-outline-danger d-none" id="deleteBtn">Delete</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="row g-2 align-items-end mb-2">
|
|
<div class="col-md-4"><label class="form-label small mb-1" for="viewFilter">Messages</label>
|
|
<input class="form-control form-control-sm" id="viewFilter" list="messageNames" placeholder="Name part or ID; commas for more; !NAME hides"></div>
|
|
<div class="col-md-2"><label class="form-label small mb-1" for="viewDirection">Direction</label>
|
|
<select class="form-select form-select-sm" id="viewDirection"><option value="">Both</option><option value="to_server">Sent by the client</option><option value="to_client">Sent to it</option></select></div>
|
|
<div class="col-md-3"><label class="form-label small mb-1" for="viewObject">Object</label>
|
|
<input class="form-control form-control-sm font-monospace" id="viewObject" placeholder="Object ID (click one in the table)"></div>
|
|
<div class="col-md-3"><label class="form-label small mb-1" for="viewSearch">Search fields</label>
|
|
<input class="form-control form-control-sm" id="viewSearch" type="search" placeholder="Text in decoded fields or hex bytes"></div>
|
|
</div>
|
|
<div class="d-flex flex-wrap gap-3 mb-2 small">
|
|
<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" id="viewDecoded"><label class="form-check-label" for="viewDecoded">Only decoded</label></div>
|
|
<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" id="viewFollow" checked data-pref="inspector.follow"><label class="form-check-label" for="viewFollow">Follow newest</label></div>
|
|
<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" id="viewRelative" data-pref="inspector.relative"><label class="form-check-label" for="viewRelative">Time since the start</label></div>
|
|
<div class="form-check form-switch mb-0"><input class="form-check-input" type="checkbox" id="viewTypes" data-pref="inspector.types"><label class="form-check-label" for="viewTypes">Counts by message</label></div>
|
|
<a href="#" class="ms-auto d-none" id="clearView">Clear filters</a>
|
|
</div>
|
|
|
|
<div class="border rounded mb-2 d-none" id="typesCard">
|
|
<div class="type-table-wrap">
|
|
<table class="table table-sm table-hover mb-0 type-table">
|
|
<thead class="sticky-top"><tr><th>Message</th><th class="text-end">ID</th><th class="text-end">Sent by the client</th><th class="text-end">Sent to it</th><th class="text-end">All</th></tr></thead>
|
|
<tbody id="typeRows"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="viewer-grid no-detail" id="viewerGrid">
|
|
<div style="min-width: 0">
|
|
<div class="msg-table-wrap border rounded" id="tableWrap" tabindex="0" aria-label="Captured messages (arrow keys move the selection)">
|
|
<table class="table table-sm table-hover mb-0 msg-table">
|
|
<thead class="sticky-top"><tr><th class="col-seq">#</th><th class="col-time">Time</th><th class="col-dir"></th><th class="col-msg">Message</th><th class="col-obj">Object</th><th class="col-bytes text-end">Bytes</th><th>Fields</th></tr></thead>
|
|
<tbody id="messageRows"></tbody>
|
|
</table>
|
|
</div>
|
|
<div class="d-flex justify-content-between small text-body-secondary mt-1"><span id="rowInfo"></span><a href="#" class="d-none" id="resumeLink"></a></div>
|
|
</div>
|
|
<div class="card detail-card d-none" id="detailCard">
|
|
<div class="card-header d-flex justify-content-between align-items-center gap-2"><h5 class="mb-0 fs-6 text-truncate" id="detailTitle"></h5>
|
|
<button type="button" class="btn-close" id="detailClose" aria-label="Close"></button></div>
|
|
<div class="card-body">
|
|
<div class="small text-body-secondary mb-2" id="detailMeta"></div>
|
|
<div id="detailFields"></div>
|
|
<div class="d-flex justify-content-between align-items-center mt-3 mb-1">
|
|
<h6 class="small text-uppercase text-body-secondary mb-0">Bytes after the object and message ID</h6>
|
|
<div class="btn-group btn-group-sm text-nowrap">
|
|
<button type="button" class="btn btn-outline-secondary" id="copyHex">Copy hex</button>
|
|
<button type="button" class="btn btn-outline-secondary" id="copyJson">Copy JSON</button>
|
|
</div>
|
|
</div>
|
|
<pre class="hexdump border rounded p-2" id="detailHex"></pre>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script src="/js/inspector.js"></script>
|
|
{% endblock %}
|