Initial implementation of Bluetooth toy control app
CI / Static checks (push) Successful in 1m27s
CI / Build and push image (push) Skipped

Next.js app with a browser-side buttplug/buttplug-wasm control layer
(server never touches real-time device commands), SQLite storage via
Drizzle, single-secret auth, recordings/replay with device remapping,
a usage stats dashboard, Docker deployment, and Gitea CI.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W8WkFF5ppURBAB918593Eb
This commit is contained in:
2026-08-25 07:51:38 +02:00
co-authored by Claude Sonnet 5
commit 1119c8eea0
112 changed files with 15522 additions and 0 deletions
+43
View File
@@ -0,0 +1,43 @@
import Database from "better-sqlite3";
import { drizzle } from "drizzle-orm/better-sqlite3";
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { getEnv } from "@/lib/env";
import * as schema from "./schema";
const globalForDb = globalThis as unknown as { __bpSqlite?: Database.Database };
function openDatabase(): Database.Database {
if (globalForDb.__bpSqlite) return globalForDb.__bpSqlite;
const path = getEnv().DATABASE_PATH;
mkdirSync(dirname(path), { recursive: true });
const instance = new Database(path);
instance.pragma("journal_mode = WAL");
instance.pragma("foreign_keys = ON");
// Reused across hot-reloads in dev so we don't leak file handles / open a
// fresh WAL-mode connection on every request.
globalForDb.__bpSqlite = instance;
return instance;
}
// Lazy by design: `next build` imports this module's whole graph while
// collecting route metadata, even for force-dynamic pages that are never
// actually rendered at build time. Opening the DB (and requiring env vars)
// as a module-level side effect would make `pnpm build` fail in CI, where
// no secrets are configured. The proxy defers both until first real query.
export const sqlite = new Proxy({} as Database.Database, {
get(_target, prop) {
// drizzle-orm's `isConfig()` probes `.constructor.name` on its first
// argument to tell a client instance from a plain config object - that
// introspection must not itself trigger opening the real connection.
if (prop === "constructor") return Database;
const instance = openDatabase();
const value = Reflect.get(instance, prop, instance);
return typeof value === "function" ? value.bind(instance) : value;
},
}) as Database.Database;
export const db = drizzle(sqlite, { schema });