Client: - Commit: batch `p4 reopen` so huge changelists (63k+) no longer blow the Windows command-line limit; live "N / total" progress + spinner during commit. - Submit dialog: white arrow, "N of total" + % bar, running file log, Cancel button (aborts pre-commit; changelist stays pending). - Disk scan: streaming reconcile with a live "found N files" counter + Cancel. - Thumbnails: only decode browser-renderable image formats; glyph fallback (+ onError) instead of a broken-image icon for bmp/tga/dds/etc. Landing + waitlist: - waitlist-server: Express + SQLite email collector (dedup) with a secure admin panel (scrypt auth, rate-limited login, signed cookies) and CSV export. - landing form now POSTs signups to the waitlist API. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
57 lines
1.8 KiB
JavaScript
57 lines
1.8 KiB
JavaScript
// SQLite storage for waitlist signups. One row per unique email — dedup is
|
|
// enforced by the DB itself (UNIQUE, case-insensitive), so a repeat signup is a
|
|
// no-op rather than a duplicate row.
|
|
|
|
import Database from "better-sqlite3";
|
|
import path from "node:path";
|
|
import fs from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = path.join(__dirname, "..", "data");
|
|
const DB_PATH = process.env.DB_PATH || path.join(DATA_DIR, "waitlist.db");
|
|
|
|
fs.mkdirSync(path.dirname(DB_PATH), { recursive: true });
|
|
|
|
const db = new Database(DB_PATH);
|
|
db.pragma("journal_mode = WAL");
|
|
db.exec(`
|
|
CREATE TABLE IF NOT EXISTS signups (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
|
source TEXT,
|
|
ip TEXT,
|
|
user_agent TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ','now'))
|
|
);
|
|
`);
|
|
|
|
const insertStmt = db.prepare(
|
|
`INSERT INTO signups (email, source, ip, user_agent)
|
|
VALUES (@email, @source, @ip, @user_agent)
|
|
ON CONFLICT(email) DO NOTHING`
|
|
);
|
|
const countStmt = db.prepare(`SELECT COUNT(*) AS n FROM signups`);
|
|
const listStmt = db.prepare(
|
|
`SELECT id, email, source, ip, created_at FROM signups ORDER BY created_at DESC, id DESC`
|
|
);
|
|
|
|
/**
|
|
* Add an email. Returns { added: true } on a new signup, { added: false } if the
|
|
* email was already on the list (deduped — no duplicate stored).
|
|
*/
|
|
export function addEmail({ email, source = null, ip = null, userAgent = null }) {
|
|
const info = insertStmt.run({ email, source, ip, user_agent: userAgent });
|
|
return { added: info.changes > 0 };
|
|
}
|
|
|
|
export function countEmails() {
|
|
return countStmt.get().n;
|
|
}
|
|
|
|
export function listEmails() {
|
|
return listStmt.all();
|
|
}
|
|
|
|
export default db;
|