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:
@@ -112,6 +112,7 @@ export const en = {
|
||||
start: "Start",
|
||||
loadStory: "Load Story",
|
||||
settings: "Settings",
|
||||
myStories: "My Stories",
|
||||
searchPlaceholder: "Search styles…",
|
||||
noMatchingStyle: "No matching styles",
|
||||
close: "Close",
|
||||
@@ -388,6 +389,22 @@ Dreamy watercolor style with soft tones and nostalgic atmosphere
|
||||
current: "Current Language",
|
||||
select: "Select Language",
|
||||
},
|
||||
|
||||
// ========== Stories Page (app/[locale]/stories/page.tsx) ==========
|
||||
stories: {
|
||||
title: "M y · S t o r i e s",
|
||||
loading: "L o a d i n g",
|
||||
emptyTitle: "No saved stories yet",
|
||||
emptyBack: "Go back home to start a new story",
|
||||
scenes: "{count} scenes",
|
||||
deleteLabel: "Delete",
|
||||
deleteConfirm: "Delete this story? This action cannot be undone.",
|
||||
deleteFailed: "Delete failed. Please try again later.",
|
||||
today: "Today",
|
||||
yesterday: "Yesterday",
|
||||
daysAgo: "{days} days ago",
|
||||
storiesCount: "{count} stories",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type EnTranslations = typeof en;
|
||||
|
||||
@@ -123,6 +123,7 @@ export const ja = {
|
||||
start: "スタート",
|
||||
loadStory: "シナリオ読み込み",
|
||||
settings: "設定",
|
||||
myStories: "マイストーリー",
|
||||
searchPlaceholder: "スタイルを検索…",
|
||||
noMatchingStyle: "一致するスタイルがありません",
|
||||
close: "閉じる",
|
||||
@@ -428,6 +429,22 @@ export const ja = {
|
||||
current: "現在の言語",
|
||||
select: "言語の選択",
|
||||
},
|
||||
|
||||
// ========== Stories Page (app/[locale]/stories/page.tsx) ==========
|
||||
stories: {
|
||||
title: "マ イ ス ト ー リ ー",
|
||||
loading: "読 み 込 み 中",
|
||||
emptyTitle: "保存されたストーリーはまだありません",
|
||||
emptyBack: "ホームに戻って新しいストーリーを始める",
|
||||
scenes: "{count}シーン",
|
||||
deleteLabel: "削除",
|
||||
deleteConfirm: "このストーリーを削除しますか?この操作は元に戻せません。",
|
||||
deleteFailed: "削除に失敗しました。後でもう一度お試しください。",
|
||||
today: "今日",
|
||||
yesterday: "昨日",
|
||||
daysAgo: "{days}日前",
|
||||
storiesCount: "{count}件",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type JaTranslations = typeof ja;
|
||||
|
||||
@@ -123,6 +123,7 @@ export const zhCN = {
|
||||
start: "开始",
|
||||
loadStory: "载入剧情",
|
||||
settings: "设置",
|
||||
myStories: "我的剧情",
|
||||
searchPlaceholder: "搜索风格…",
|
||||
noMatchingStyle: "没有匹配的风格",
|
||||
close: "关闭",
|
||||
@@ -428,6 +429,22 @@ export const zhCN = {
|
||||
current: "当前语言",
|
||||
select: "选择语言",
|
||||
},
|
||||
|
||||
// ========== Stories Page (app/[locale]/stories/page.tsx) ==========
|
||||
stories: {
|
||||
title: "我 · 的 · 剧 · 情",
|
||||
loading: "载 · 入 · 中",
|
||||
emptyTitle: "还没有保存的剧情",
|
||||
emptyBack: "回到首页开始新的故事",
|
||||
scenes: "{count} 幕",
|
||||
deleteLabel: "删除",
|
||||
deleteConfirm: "确认删除这个剧情?此操作无法撤销。",
|
||||
deleteFailed: "删除失败,请稍后重试",
|
||||
today: "今天",
|
||||
yesterday: "昨天",
|
||||
daysAgo: "{days} 天前",
|
||||
storiesCount: "{count} 个剧情",
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type ZhCNTranslations = typeof zhCN;
|
||||
|
||||
Reference in New Issue
Block a user