- Fix device rename input collapsing on mobile (fixed width vs w-full inside an auto-layout table column). - Add cascade-delete confirmation for sessions with a saved recording. - Fix header connection LED: derive state from scanning/device-count/ recording instead of the Buttplug client's raw connected flag; drop the label text and hide the indicator entirely when idle. - Add a per-device disconnect button (stop + remove from store, the closest equivalent Buttplug's protocol allows per device). - Fix replay ending early: duration now comes from the recording's actual durationMs, not the last event's timestamp. - Replay robustness: show which devices are being replayed to, reset actuators to zero on start/play/pause, fully disconnect devices and the whole client on stop/unmount, surface command failures via toast. - Stop flagging the header LED red during replay - recording is only for live sessions. - Add page-number pagination to recordings/sessions/devices lists. - Wordmark SEXY -> Sexy; add proper per-page <title>s including dynamic titles for recording/session detail and replay pages. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
65 lines
2.0 KiB
TypeScript
65 lines
2.0 KiB
TypeScript
import { desc, eq, sql } from "drizzle-orm";
|
|
import { db } from "@/lib/db/client";
|
|
import { devices, type DeviceCapabilities } from "@/lib/db/schema";
|
|
import { PAGE_SIZE, type Page } from "@/lib/pagination";
|
|
|
|
export async function listDevices() {
|
|
return db.select().from(devices).orderBy(devices.lastConnectedAt);
|
|
}
|
|
|
|
export async function listDevicesPage(page: number, pageSize = PAGE_SIZE): Promise<Page<typeof devices.$inferSelect>> {
|
|
const [{ count }] = await db.select({ count: sql<number>`count(*)` }).from(devices);
|
|
const items = await db
|
|
.select()
|
|
.from(devices)
|
|
.orderBy(desc(devices.lastConnectedAt))
|
|
.limit(pageSize)
|
|
.offset((page - 1) * pageSize);
|
|
return { items, page, pageSize, total: count };
|
|
}
|
|
|
|
export async function getDevice(id: number) {
|
|
const [row] = await db.select().from(devices).where(eq(devices.id, id));
|
|
return row;
|
|
}
|
|
|
|
/** Upserts by advertised BLE name - the only stable-ish identity Web Bluetooth exposes. */
|
|
export async function upsertDeviceByBleName(input: {
|
|
bleName: string;
|
|
deviceClass?: string | null;
|
|
capabilities?: DeviceCapabilities;
|
|
}): Promise<typeof devices.$inferSelect> {
|
|
const now = Date.now();
|
|
const [existing] = await db.select().from(devices).where(eq(devices.bleName, input.bleName));
|
|
|
|
if (existing) {
|
|
const [updated] = await db
|
|
.update(devices)
|
|
.set({
|
|
lastConnectedAt: now,
|
|
deviceClass: input.deviceClass ?? existing.deviceClass,
|
|
capabilities: input.capabilities ?? existing.capabilities,
|
|
})
|
|
.where(eq(devices.id, existing.id))
|
|
.returning();
|
|
return updated;
|
|
}
|
|
|
|
const [created] = await db
|
|
.insert(devices)
|
|
.values({
|
|
bleName: input.bleName,
|
|
deviceClass: input.deviceClass,
|
|
capabilities: input.capabilities,
|
|
createdAt: now,
|
|
lastConnectedAt: now,
|
|
})
|
|
.returning();
|
|
return created;
|
|
}
|
|
|
|
export async function renameDevice(id: number, displayName: string) {
|
|
const [updated] = await db.update(devices).set({ displayName }).where(eq(devices.id, id)).returning();
|
|
return updated;
|
|
}
|