Persist byte-level I/O events from SSH sessions to SQLite and add a web
UI to replay them with original timing. Events are buffered in memory
and flushed every 2s to avoid blocking SSH I/O on database writes.
- Add session_events table (migration 002)
- Add SessionEvent type and storage methods (SQLite + MemoryStore)
- Change RecordingChannel to support multiple callbacks
- Add EventRecorder for buffered event persistence
- Add session detail page with xterm.js terminal replay
- Add /api/sessions/{id}/events JSON endpoint
- Linkify session IDs in dashboard and active sessions
- Vendor xterm.js v5.3.0
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.7 KiB
HTML
80 lines
2.7 KiB
HTML
{{define "content"}}
|
|
<section>
|
|
<h3>Session {{.Session.ID}}</h3>
|
|
<div class="top-grid">
|
|
<article>
|
|
<header>Session Info</header>
|
|
<table>
|
|
<tbody>
|
|
<tr><td><strong>IP</strong></td><td>{{.Session.IP}}</td></tr>
|
|
<tr><td><strong>Username</strong></td><td>{{.Session.Username}}</td></tr>
|
|
<tr><td><strong>Shell</strong></td><td>{{.Session.ShellName}}</td></tr>
|
|
<tr><td><strong>Score</strong></td><td>{{formatScore .Session.HumanScore}}</td></tr>
|
|
<tr><td><strong>Connected</strong></td><td>{{formatTime .Session.ConnectedAt}}</td></tr>
|
|
<tr>
|
|
<td><strong>Disconnected</strong></td>
|
|
<td>{{if .Session.DisconnectedAt}}{{formatTime (derefTime .Session.DisconnectedAt)}}{{else}}<mark>active</mark>{{end}}</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</article>
|
|
</div>
|
|
</section>
|
|
|
|
{{if gt .EventCount 0}}
|
|
<section>
|
|
<h3>Session Replay</h3>
|
|
<div style="margin-bottom: 1rem;">
|
|
<button id="btn-play" onclick="replayPlayer.play()">Play</button>
|
|
<button id="btn-pause" onclick="replayPlayer.pause()">Pause</button>
|
|
<button id="btn-reset" onclick="replayPlayer.reset()">Reset</button>
|
|
<label for="speed-select" style="margin-left: 1rem;">Speed:</label>
|
|
<select id="speed-select" onchange="replayPlayer.setSpeed(parseFloat(this.value))">
|
|
<option value="0.5">0.5x</option>
|
|
<option value="1" selected>1x</option>
|
|
<option value="2">2x</option>
|
|
<option value="5">5x</option>
|
|
<option value="10">10x</option>
|
|
</select>
|
|
</div>
|
|
<div id="terminal" style="background: #000; padding: 4px; border-radius: 4px;"></div>
|
|
</section>
|
|
<link rel="stylesheet" href="/static/xterm.css">
|
|
<script src="/static/xterm.min.js"></script>
|
|
<script src="/static/replay.js"></script>
|
|
<script>
|
|
var replayPlayer = new ReplayPlayer("terminal", "{{.Session.ID}}");
|
|
</script>
|
|
{{else}}
|
|
<section>
|
|
<p>No recorded events for this session.</p>
|
|
</section>
|
|
{{end}}
|
|
|
|
{{if .Logs}}
|
|
<section>
|
|
<h3>Command Log</h3>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>Input</th>
|
|
<th>Output</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{{range .Logs}}
|
|
<tr>
|
|
<td>{{formatTime .Timestamp}}</td>
|
|
<td><code>{{.Input}}</code></td>
|
|
<td><pre style="margin:0; white-space:pre-wrap;">{{.Output}}</pre></td>
|
|
</tr>
|
|
{{end}}
|
|
</tbody>
|
|
</table>
|
|
</section>
|
|
{{end}}
|
|
|
|
<p><a href="/">← Back to dashboard</a></p>
|
|
{{end}}
|