Remove recordings feature, replay sessions directly, bump to 0.6.0
CI / Build and push image (push) Successful in 1m41s
CI / Static checks (push) Successful in 2m12s

Recordings were just a thin named pointer over an already-captured
session's events, so the whole separate feature (recordings table, API
routes, pages, UI) is gone: any completed session can now be named and
replayed directly. Replaying no longer creates a session or duplicates
events of its own - it just bumps the source session's playCount/lastPlayedAt.

Also renames play_sessions/playSession(s) to sessions/session(s) throughout
the schema, queries, API routes, and UI for consistency, and updates the
README to match the new flow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 21:38:51 +02:00
co-authored by Claude Sonnet 5
parent 2a3c4ff1f2
commit 401b9b5033
46 changed files with 1710 additions and 1018 deletions
+12 -13
View File
@@ -13,21 +13,20 @@ interface ApiEvent {
/**
* Buffers every dispatched command (live or replay) and flushes it in
* batches to the play-session's events endpoint, so a dragged slider never
* batches to the session's events endpoint, so a dragged slider never
* fires one HTTP request per tick. Every command is always recorded here
* regardless of whether the session is later saved as a named recording -
* "recording" is a save decision made after the fact, not a separate
* capture pipeline (see lib/db/queries/recordings.ts).
* regardless of whether the session is later named - any completed session
* can be replayed directly, there's no separate "save as recording" step.
*/
class EventBuffer {
private buffer: CommandEvent[] = [];
private playSessionId: number | null = null;
private sessionId: number | null = null;
private sessionStartedAt = 0;
private sessionDeviceIdByDeviceIndex = new Map<number, number>();
private timer: ReturnType<typeof setInterval> | null = null;
start(playSessionId: number, sessionStartedAt: number): void {
this.playSessionId = playSessionId;
start(sessionId: number, sessionStartedAt: number): void {
this.sessionId = sessionId;
this.sessionStartedAt = sessionStartedAt;
this.sessionDeviceIdByDeviceIndex = new Map();
this.buffer = [];
@@ -46,16 +45,16 @@ class EventBuffer {
}
record(event: Omit<CommandEvent, "tsMs"> & { tsMs?: number }): void {
if (this.playSessionId === null) return;
if (this.sessionId === null) return;
const tsMs = event.tsMs ?? Date.now() - this.sessionStartedAt;
this.buffer.push({ ...event, tsMs });
}
async flush(): Promise<void> {
if (this.buffer.length === 0 || this.playSessionId === null) return;
if (this.buffer.length === 0 || this.sessionId === null) return;
const events = this.drain();
try {
await fetch(`/api/play-sessions/${this.playSessionId}/events`, {
await fetch(`/api/sessions/${this.sessionId}/events`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ events }),
@@ -71,7 +70,7 @@ class EventBuffer {
void this.flush();
if (this.timer) clearInterval(this.timer);
this.timer = null;
this.playSessionId = null;
this.sessionId = null;
if (typeof window !== "undefined") {
window.removeEventListener("beforeunload", this.flushBeacon);
document.removeEventListener("visibilitychange", this.onVisibilityChange);
@@ -83,10 +82,10 @@ class EventBuffer {
};
private flushBeacon = (): void => {
if (this.buffer.length === 0 || this.playSessionId === null || typeof navigator === "undefined") return;
if (this.buffer.length === 0 || this.sessionId === null || typeof navigator === "undefined") return;
const events = this.drain();
navigator.sendBeacon(
`/api/play-sessions/${this.playSessionId}/events`,
`/api/sessions/${this.sessionId}/events`,
new Blob([JSON.stringify({ events })], { type: "application/json" }),
);
};