Files
sexy/app/(app)/sessions/[id]/page.tsx
T
valknarandClaude Sonnet 5 401b9b5033
CI / Build and push image (push) Successful in 1m41s
CI / Static checks (push) Successful in 2m12s
Remove recordings feature, replay sessions directly, bump to 0.6.0
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>
2026-08-27 21:38:51 +02:00

88 lines
3.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Link from "next/link";
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { SessionTimelineChart } from "@/components/sessions/SessionTimelineChart";
import { getSessionDetail, getSessionName } from "@/lib/db/queries/sessions";
import { getSessionTimeline } from "@/lib/db/queries/stats";
import { Play } from "lucide-react";
export const dynamic = "force-dynamic";
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
const { id } = await params;
const name = await getSessionName(Number(id));
return { title: name ?? `Session #${id}` };
}
function formatDuration(ms: number | null): string {
if (ms === null) return "-";
const totalSeconds = Math.round(ms / 1000);
return `${Math.floor(totalSeconds / 60)}:${(totalSeconds % 60).toString().padStart(2, "0")}`;
}
export default async function SessionDetailPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const sessionId = Number(id);
const [detail, timeline] = await Promise.all([getSessionDetail(sessionId), getSessionTimeline(sessionId)]);
if (!detail) notFound();
const canReplay = detail.session.status === "completed" && detail.session.durationMs !== null;
return (
<div className="flex flex-col gap-6">
<div className="flex items-center justify-between gap-4">
<div>
<h1 className="font-heading text-2xl font-semibold">{detail.session.name ?? `Session #${detail.session.id}`}</h1>
<p className="text-sm text-muted-foreground">
{detail.session.status} · {formatDuration(detail.session.durationMs)}
{detail.session.kind === "replay" && detail.replayedFromName !== undefined && (
<> · replayed from {detail.replayedFromName ?? `session #${detail.session.replayedSessionId}`}</>
)}
{detail.session.playCount > 0 && (
<>
{" "}
· replayed {detail.session.playCount}×
{detail.session.lastPlayedAt && ` (last ${new Date(detail.session.lastPlayedAt).toLocaleString()})`}
</>
)}
</p>
{detail.session.description && (
<p className="mt-1 text-sm text-muted-foreground">{detail.session.description}</p>
)}
</div>
{canReplay && (
<Button asChild>
<Link href={`/sessions/${detail.session.id}/replay`}>
<Play className="size-4" /> Replay
</Link>
</Button>
)}
</div>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-base">Intensity timeline</CardTitle>
</CardHeader>
<CardContent>
<SessionTimelineChart timeline={timeline} />
</CardContent>
</Card>
<Card className="bp-glass">
<CardHeader>
<CardTitle className="text-base">Devices</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-1">
{detail.devices.map((d) => (
<div key={d.id} className="text-sm">
{d.slotLabel} <span className="text-muted-foreground">({d.deviceDisplayName ?? d.deviceBleName})</span>
</div>
))}
</CardContent>
</Card>
</div>
);
}