feat(persistence): local-first story persistence (IndexedDB + Supabase skeleton)
Remove Cloudflare D1 entirely (4 API routes, lib/db/, Drizzle config/migrations, drizzle-orm/drizzle-kit deps, wrangler D1/R2/KV bindings) and replace with browser-local-first architecture: Open-source build (IndexedDB, no auth): - lib/persistence/ 5-file module: types, idb adapter (zero-dep, fault-tolerant, post-open invalidation retry), localStore (CRUD + sync-reserved metadata + slim/rebuild + retention-cap eviction with tombstone reap + sync-state protection + last-resort bounded fallback), sessionSlim (voice strip + styleRef absent-delete), cloudStore (Supabase skeleton, server-only) - Autosave: persistence fingerprint (history.length:lastBeatCount:playerName), serial saveChain, failure rollback retry, replaySourceRef guard (prevents replayed shared stories from clobbering user saves) - clientStoryPersistence.ts: thin facade (SaveResult discriminated union) - Stories page: /[locale]/stories with 3-language i18n (zh-CN/en/ja) - Homepage: book icon entry point in header Commercial build (Supabase, skeleton only): - Single table public.stories (JSONB + RLS 4 policies on auth.uid()=user_id) - supabase/migrations/ DDL (idempotent) - cloudStore.ts server-only repository, AUTH_ENABLED short-circuit - Not wired to client this phase Featured stories: pure fallback (buildFallbackCards + localizeCards), no D1 Includes fixes from 3 rounds of subagent code-review (tasks 16-30): - CR1: autosave restructure, coerceOrientation, D1 comment cleanup - CR2: fingerprint+serial+rollback+replay guard, idb post-open retry, enforceRetentionCap latent defense, sessionSlim absent invariant - CR3: single-scene share guard (replaySourceRef), insert-beat fingerprint (beats.length), pass3 overflow double-count fix, detach gate unification
This commit is contained in:
@@ -1,31 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET/DELETE /api/stories/[id] — TEMPORARILY DISABLED (2026-06-09)
|
||||
*
|
||||
* D1 persistence disabled until authentication integration.
|
||||
* Returns 404 so client handles gracefully (localStorage is the source of truth).
|
||||
*
|
||||
* To re-enable: Restore original implementation after auth integration.
|
||||
*/
|
||||
export async function GET(
|
||||
_req: Request,
|
||||
_context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Server persistence temporarily disabled" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
_req: Request,
|
||||
_context: { params: Promise<{ id: string }> },
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Server persistence temporarily disabled" },
|
||||
{ status: 404 },
|
||||
);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { getDb } from "@/lib/db/client";
|
||||
import { FeaturedRepository } from "@/lib/db/repositories/featuredRepo";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET /api/stories/featured?gender=male
|
||||
*
|
||||
* List active featured stories for homepage display.
|
||||
* Fallback: D1 query fails → return empty array (homepage shows no cards, gracefully degrades).
|
||||
*
|
||||
* Query Params:
|
||||
* gender: "male" | "female" (required)
|
||||
*
|
||||
* Response: { stories: FeaturedStory[] }
|
||||
* Errors: 400 (invalid gender), 500 (should not reach user - caught and degraded)
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
const { searchParams } = new URL(req.url);
|
||||
const genderParam = searchParams.get("gender");
|
||||
|
||||
// Validate gender
|
||||
if (!genderParam || !["male", "female"].includes(genderParam)) {
|
||||
return NextResponse.json(
|
||||
{ error: "gender query parameter must be 'male' or 'female'" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const gender = genderParam as "male" | "female";
|
||||
|
||||
try {
|
||||
const db = getDb();
|
||||
const repo = new FeaturedRepository(db);
|
||||
|
||||
const stories = await repo.listByGender(gender);
|
||||
|
||||
return NextResponse.json({ stories });
|
||||
} catch (err) {
|
||||
// D1 unavailable or query failed - degrade to empty array
|
||||
// (homepage will show no cards but remain functional)
|
||||
const message = err instanceof Error ? err.message : "Unknown error";
|
||||
console.error("[stories/featured] D1 query failed, returning empty array:", message);
|
||||
|
||||
return NextResponse.json({ stories: [] });
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* GET /api/stories/list — TEMPORARILY DISABLED (2026-06-09)
|
||||
*
|
||||
* D1 persistence disabled until authentication integration.
|
||||
* Returns empty list so client falls back to localStorage-only mode.
|
||||
*
|
||||
* To re-enable: Restore original implementation after auth integration.
|
||||
*/
|
||||
export async function GET(_req: Request) {
|
||||
return NextResponse.json({ stories: [] });
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
/**
|
||||
* POST /api/stories/save — TEMPORARILY DISABLED (2026-06-09)
|
||||
*
|
||||
* D1 persistence is disabled until an authentication system (better-auth) is
|
||||
* integrated. Without auth, anonymous writes to D1 have no rate limiting,
|
||||
* per-user quota, or ownership verification — an abuse/DoS risk on a public,
|
||||
* registration-less site. The client (lib/clientStoryPersistence.ts) now
|
||||
* persists stories to localStorage only; this 503 keeps the contract intact
|
||||
* for any caller that still hits the endpoint.
|
||||
*
|
||||
* The full D1 implementation lives in StoryRepository (lib/db/repositories/
|
||||
* storyRepo.ts), which is untouched. To re-enable after auth integration:
|
||||
* restore the handler to validate input + call `repo.save(...)` (see the
|
||||
* task-10 implementation log) and gate it behind an authenticated session.
|
||||
*
|
||||
* See: ARCHITECTURE_DESIGN.md Phase 2, memory tech_d1_anonymous_write_risk
|
||||
*/
|
||||
export async function POST(_req: Request) {
|
||||
return NextResponse.json(
|
||||
{ error: "Server persistence temporarily disabled - using local storage" },
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user