pi-remote-control/extensions/remote-control/server/routes/sessions.ts

169 lines
4.6 KiB
TypeScript
Raw Permalink 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.

/**
* S-09 — multi-session CRUD routes.
*
* POST /sessions → { id, name, state, lastOutputAt }
* GET /sessions → [{ id, name, description, state, lastOutputAt }]
* PATCH /sessions/:id → updates @description
* DELETE /sessions/:id → kills tmux session, optionally clears buffer
* GET /sessions/:id/thumbnail → text/plain capture-pane (40×12)
*
* Owner: T-1.6
*/
import type { IncomingMessage, ServerResponse } from "node:http";
import { BufferWriter } from "../../buffer/writer.js";
import {
getSession,
killSession,
listSessions,
setDescription,
spawnSession,
} from "../../tmux/manager.js";
import { captureThumbnail } from "../../tmux/snapshot.js";
import { readBody, sendJson } from "../util.js";
export async function handleSessions(
req: IncomingMessage,
res: ServerResponse,
sessionId?: string,
sub?: string,
): Promise<void> {
const method = req.method?.toUpperCase();
// GET /sessions/:id/thumbnail
if (sessionId && sub === "thumbnail" && method === "GET") {
const session = await getSession(sessionId).catch(() => null);
if (!session) {
sendJson(res, 404, { error: "session_not_found" });
return;
}
const text = await captureThumbnail(sessionId).catch(() => "");
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
res.end(text);
return;
}
// /sessions/:id — PATCH / DELETE
if (sessionId && !sub) {
if (method === "PATCH") {
await handlePatch(req, res, sessionId);
return;
}
if (method === "DELETE") {
await handleDelete(req, res, sessionId);
return;
}
sendJson(res, 405, { error: "method_not_allowed" });
return;
}
// /sessions — GET / POST
if (!sessionId) {
if (method === "GET") {
await handleList(res);
return;
}
if (method === "POST") {
await handleCreate(req, res);
return;
}
sendJson(res, 405, { error: "method_not_allowed" });
return;
}
sendJson(res, 404, { error: "not_found" });
}
async function handleList(res: ServerResponse): Promise<void> {
try {
const sessions = await listSessions();
const payload = sessions.map((s) => ({
id: s.id,
name: s.name,
description: s.description,
state: "idle", // T-1.4 events will feed real state in Phase 2
lastOutputAt: s.lastActivityAt,
}));
sendJson(res, 200, payload);
} catch (err) {
sendJson(res, 500, { error: "internal_error", message: String(err) });
}
}
async function handleCreate(
req: IncomingMessage,
res: ServerResponse,
): Promise<void> {
let body: Record<string, unknown> = {};
try {
const raw = await readBody(req);
if (raw.trim()) body = JSON.parse(raw) as Record<string, unknown>;
} catch {
sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
return;
}
const name =
typeof body.name === "string" && body.name.trim()
? body.name.trim()
: `session-${Date.now()}`;
try {
const id = await spawnSession({ name });
// Include state + lastOutputAt to match the GET /sessions response shape
// so iOS clients can decode the response with the same type.
sendJson(res, 201, { id, name, state: "idle", lastOutputAt: "" });
} catch (err) {
sendJson(res, 500, { error: "internal_error", message: String(err) });
}
}
async function handlePatch(
req: IncomingMessage,
res: ServerResponse,
sessionId: string,
): Promise<void> {
const session = await getSession(sessionId).catch(() => null);
if (!session) {
sendJson(res, 404, { error: "session_not_found" });
return;
}
let body: Record<string, unknown> = {};
try {
const raw = await readBody(req);
if (raw.trim()) body = JSON.parse(raw) as Record<string, unknown>;
} catch {
sendJson(res, 400, { error: "bad_request", message: "Invalid JSON" });
return;
}
if (typeof body.description === "string") {
await setDescription(sessionId, body.description);
}
sendJson(res, 200, { id: sessionId, description: body.description });
}
async function handleDelete(
_req: IncomingMessage,
res: ServerResponse,
sessionId: string,
): Promise<void> {
const session = await getSession(sessionId).catch(() => null);
if (!session) {
sendJson(res, 404, { error: "session_not_found" });
return;
}
try {
await killSession(sessionId);
// Optionally clear buffer
const writer = new BufferWriter(sessionId);
await writer.delete().catch(() => {}); // best-effort
res.writeHead(204).end();
} catch (err) {
sendJson(res, 500, { error: "internal_error", message: String(err) });
}
}