Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 51 additions & 11 deletions components/settings/IdbUnlockModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,43 +12,83 @@ interface Props {

const ATTEMPT_STORAGE_KEY = 'worldscript-idb-unlock-attempts';
const LOCKOUT_STORAGE_KEY = 'worldscript-idb-unlock-lockout';
Comment thread
qnbs marked this conversation as resolved.
const LEGACY_ATTEMPT_STORAGE_KEY = 'storycraft-idb-unlock-attempts';
const LEGACY_LOCKOUT_STORAGE_KEY = 'storycraft-idb-unlock-lockout';

function getAttemptCount(): number {
// QNBS-v3: lockout backoff is capped at 60s (see lockoutMs), so a valid lockout timestamp never
// exceeds now + 60s; the attempt counter is a tiny integer. These bounds clamp corrupt storage.
const MAX_LOCKOUT_MS = 60_000;
const MAX_ATTEMPTS = 1000;

function readInt(key: string, legacyKey: string, max = Number.MAX_SAFE_INTEGER): number {
if (typeof window === 'undefined') {
return 0;
}
try {
return Number.parseInt(localStorage.getItem(ATTEMPT_STORAGE_KEY) ?? '0', 10);
let raw = window.localStorage.getItem(key);
// QNBS-v3: Rebrand migration — read legacy StoryCraft lockout state once, then move to the new key.
if (raw === null) {
raw = window.localStorage.getItem(legacyKey);
if (raw !== null) {
window.localStorage.setItem(key, raw);
window.localStorage.removeItem(legacyKey);
}
Comment thread
qnbs marked this conversation as resolved.
}
// QNBS-v3: corrupt/non-numeric storage values parse to NaN, which would poison the lockout
// math (Date.now() + NaN) and the countdown UI — normalize any invalid parse to 0.
const parsed = Number.parseInt(raw ?? '0', 10);
if (Number.isNaN(parsed)) return 0;
// QNBS-v3 (CodeAnt): clamp to a non-negative, bounded range so a corrupt/huge value — e.g. a
// far-future lockout timestamp — cannot block unlock indefinitely.
return Math.min(Math.max(parsed, 0), max);
} catch {
return 0;
}
}

function getAttemptCount(): number {
return readInt(ATTEMPT_STORAGE_KEY, LEGACY_ATTEMPT_STORAGE_KEY, MAX_ATTEMPTS);
}

function setAttemptCount(n: number): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.setItem(ATTEMPT_STORAGE_KEY, String(n));
window.localStorage.setItem(ATTEMPT_STORAGE_KEY, String(n));
Comment thread
qnbs marked this conversation as resolved.
} catch {
/* storage blocked */
}
}

function getLockoutUntil(): number {
try {
return Number.parseInt(localStorage.getItem(LOCKOUT_STORAGE_KEY) ?? '0', 10);
} catch {
return 0;
}
const ts = readInt(LOCKOUT_STORAGE_KEY, LEGACY_LOCKOUT_STORAGE_KEY);
// QNBS-v3 (CodeAnt): a valid lockout never exceeds now + the max backoff window. Treat a
// beyond-bound (corrupt) value as INVALID → no lockout (0). A moving `now + MAX` clamp would
// re-clamp every read and never count down, permanently locking the user out.
return ts > Date.now() + MAX_LOCKOUT_MS ? 0 : ts;
}

function setLockoutUntil(ts: number): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.setItem(LOCKOUT_STORAGE_KEY, String(ts));
window.localStorage.setItem(LOCKOUT_STORAGE_KEY, String(ts));
} catch {
/* storage blocked */
}
}

function clearAttemptTracking(): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.removeItem(ATTEMPT_STORAGE_KEY);
localStorage.removeItem(LOCKOUT_STORAGE_KEY);
window.localStorage.removeItem(ATTEMPT_STORAGE_KEY);
window.localStorage.removeItem(LOCKOUT_STORAGE_KEY);
window.localStorage.removeItem(LEGACY_ATTEMPT_STORAGE_KEY);
window.localStorage.removeItem(LEGACY_LOCKOUT_STORAGE_KEY);
} catch {
/* storage blocked */
}
Expand Down
6 changes: 4 additions & 2 deletions hooks/useTemplateView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,8 +125,10 @@ export const useTemplateView = ({ onNavigate }: UseTemplateViewProps) => {
const newManuscript: StorySection[] = ct.sections.map((s, i) => ({
id: `sec-${stamp}-${i}`,
title: s.title,
content: s.description ? `# ${s.title}\n\n${s.description}` : '',
prompt: '',
// QNBS-v3: section guidance goes into `prompt` (writing brief), NOT `content` — mirrors the
// built-in-template path so a freshly applied template starts with an EMPTY manuscript.
content: '',
prompt: s.description ?? '',
}));
const newOutline: OutlineSection[] = ct.sections.map((s, i) => ({
id: `out-${stamp}-${i}`,
Expand Down
3 changes: 2 additions & 1 deletion locales/ar/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "فشل التحميل: {{error}}",
"voice.modelDownload.progress": "{{percent}}% مكتمل",
"voice.modelDownload.retry": "إعادة المحاولة",
"voice.modelDownload.title": "تحميل نموذج الصوت"
"voice.modelDownload.title": "تحميل نموذج الصوت",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Download fehlgeschlagen: {{error}}",
"voice.modelDownload.progress": "{{percent}}% abgeschlossen",
"voice.modelDownload.retry": "Wiederholen",
"voice.modelDownload.title": "Sprachmodell-Download"
"voice.modelDownload.title": "Sprachmodell-Download",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/el/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Η λήψη απέτυχε: {{error}}",
"voice.modelDownload.progress": "{{percent}}% ολοκληρώθηκε",
"voice.modelDownload.retry": "Δοκιμάζω πάλι",
"voice.modelDownload.title": "Λήψη φωνητικού μοντέλου"
"voice.modelDownload.title": "Λήψη φωνητικού μοντέλου",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"settings.ai.localAi.modelLabel.phi4mini": "Phi-4 Mini 3.8B (~2.3 GB)",
"settings.ai.localAi.modelLabel.gemma3_1b": "Gemma 3 1B (~0.8 GB)",
"settings.ai.localAi.modelLabel.gemma3_4b": "Gemma 3 4B (~4.9 GB)",
"settings.ai.localAi.modelLabel.llama33_70b": "Llama 3.3 70B (high-end, ~35 GB)"
"settings.ai.localAi.modelLabel.llama33_70b": "Llama 3.3 70B (high-end, ~35 GB)",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Descarga fallida: {{error}}",
"voice.modelDownload.progress": "{{percent}}% completado",
"voice.modelDownload.retry": "Reintentar",
"voice.modelDownload.title": "Descarga de modelo de voz"
"voice.modelDownload.title": "Descarga de modelo de voz",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Échec du téléchargement : {{error}}",
"voice.modelDownload.progress": "{{percent}}% terminé",
"voice.modelDownload.retry": "Réessayer",
"voice.modelDownload.title": "Téléchargement du modèle vocal"
"voice.modelDownload.title": "Téléchargement du modèle vocal",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/he/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "ההורדה נכשלה: {{error}}",
"voice.modelDownload.progress": "{{percent}}% הושלם",
"voice.modelDownload.retry": "נסה שוב",
"voice.modelDownload.title": "הורדת מודל קול"
"voice.modelDownload.title": "הורדת מודל קול",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/it/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Download fallito: {{error}}",
"voice.modelDownload.progress": "{{percent}}% completato",
"voice.modelDownload.retry": "Riprova",
"voice.modelDownload.title": "Download modello vocale"
"voice.modelDownload.title": "Download modello vocale",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/ja/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "ダウンロードに失敗しました: {{error}}",
"voice.modelDownload.progress": "{{percent}}% 完了",
"voice.modelDownload.retry": "リトライ",
"voice.modelDownload.title": "音声モデルのダウンロード"
"voice.modelDownload.title": "音声モデルのダウンロード",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/pt/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "Falha no download: {{error}}",
"voice.modelDownload.progress": "{{percent}}% concluído",
"voice.modelDownload.retry": "Tentar novamente",
"voice.modelDownload.title": "Download do modelo de voz"
"voice.modelDownload.title": "Download do modelo de voz",
"settings.about.productName": "WorldScript Studio"
}
3 changes: 2 additions & 1 deletion locales/zh/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -797,5 +797,6 @@
"voice.modelDownload.error": "下载失败:{{error}}",
"voice.modelDownload.progress": "{{percent}}% 完成",
"voice.modelDownload.retry": "重试",
"voice.modelDownload.title": "语音模型下载"
"voice.modelDownload.title": "语音模型下载",
"settings.about.productName": "WorldScript Studio"
}
1 change: 1 addition & 0 deletions public/locales/ar/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/de/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/el/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/en/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/es/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/fr/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/he/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions public/locales/it/bundle.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading