Writingway2/restore.html

256 lines
No EOL
11 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Writingway DB Restore Utility</title>
<style>
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial;
padding: 18px;
color: #111;
background: #f7f7fb
}
h1 {
margin-top: 0
}
.panel {
background: white;
padding: 12px;
border-radius: 8px;
box-shadow: 0 1px 4px rgba(0, 0, 0, .06);
margin-bottom: 12px
}
textarea {
width: 100%;
height: 260px;
font-family: monospace;
font-size: 13px;
}
button {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid #ccc;
background: #fff;
cursor: pointer
}
button.primary {
background: #0069ff;
color: white;
border-color: #0069ff
}
button.warn {
background: #ff6b6b;
color: white;
border-color: #ff6b6b
}
pre {
background: #0f1724;
color: #d1d5db;
padding: 10px;
border-radius: 6px;
overflow: auto;
height: 160px
}
.row {
display: flex;
gap: 8px;
align-items: center
}
.muted {
color: #6b7280
}
</style>
<script src="https://cdn.jsdelivr.net/npm/dexie@3.2.4/dist/dexie.min.js"></script>
</head>
<body>
<h1>Writingway — DB Restore Utility</h1>
<div class="panel">
<div class="muted">This tool helps you safely delete the existing <code>WritingwayDB</code> in this browser
profile and restore it from a JSON dump you paste below. <strong>Do not proceed unless you have the JSON
dump copied from the app console (between JSON_START and JSON_END)</strong>.</div>
</div>
<div class="panel">
<h3>1) Paste JSON dump</h3>
<textarea id="dump" placeholder="Paste JSON here (between JSON_START and JSON_END)"></textarea>
<div style="margin-top:8px;display:flex;gap:8px">
<button id="validate">Validate JSON</button>
<button id="download">Download dump as file</button>
<span id="validateResult" class="muted"></span>
</div>
</div>
<div class="panel">
<h3>2) Delete DB and restore</h3>
<div class="muted">When you click <strong>Delete and Restore</strong> the script will:</div>
<ol>
<li>Confirm you want to proceed.</li>
<li>Delete <code>WritingwayDB</code> (this is the destructive step).</li>
<li>Create a fresh DB and import the pasted data.</li>
<li>Set <code>localStorage['writingway:lastProject']</code> to the first project id (if present).</li>
</ol>
<div style="margin-top:8px;display:flex;gap:8px">
<button id="deleteRestore" class="warn">Delete DB and Restore</button>
<button id="createBackupDb">Create recovered DB (non-destructive)</button>
<span id="opResult" class="muted"></span>
</div>
</div>
<div class="panel">
<h3>Logs</h3>
<pre id="log">Ready.</pre>
</div>
<script>
const dumpEl = document.getElementById('dump');
const logEl = document.getElementById('log');
const validateEl = document.getElementById('validate');
const downloadEl = document.getElementById('download');
const deleteRestoreEl = document.getElementById('deleteRestore');
const createBackupDbEl = document.getElementById('createBackupDb');
const validateResult = document.getElementById('validateResult');
function log(...args) { console.log(...args); logEl.textContent += '\n' + args.map(a => (typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a))).join(' '); logEl.scrollTop = logEl.scrollHeight }
function parseDump() {
const txt = dumpEl.value.trim();
if (!txt) throw new Error('No JSON present in textarea');
try { return JSON.parse(txt); } catch (e) { throw new Error('Invalid JSON: ' + (e && e.message)); }
}
validateEl.addEventListener('click', () => {
validateResult.textContent = '';
try {
const dump = parseDump();
const keys = Object.keys(dump);
validateResult.textContent = 'Valid JSON — stores: ' + keys.join(', ');
log('Validated dump stores:', keys);
} catch (e) {
validateResult.textContent = 'Invalid JSON — see logs';
log('Validate error', e.message || e);
}
});
downloadEl.addEventListener('click', () => {
try {
const txt = dumpEl.value;
if (!txt) { alert('Paste the JSON dump first'); return; }
const blob = new Blob([txt], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = 'writingway_dump.json'; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);
log('Downloaded dump to writingway_dump.json');
} catch (e) { log('Download error', e); }
});
async function performRestore(opts = { dbName: 'WritingwayDB', createBackup: false, backupName: 'WritingwayDB_recovered' }) {
try {
const dump = parseDump();
log('Parsed dump stores:', Object.keys(dump));
if (opts.createBackup) {
// create a new DB name and populate it without deleting the original
const dbName = opts.backupName || 'WritingwayDB_recovered';
log('Creating backup DB (non-destructive) named', dbName);
await populateDexie(dbName, dump);
log('Backup DB created:', dbName);
return { ok: true };
}
// Confirm
const c1 = confirm('This will DELETE the existing IndexedDB database "WritingwayDB" in this browser profile and replace it with the pasted dump. Are you sure?');
if (!c1) { log('User aborted'); return { ok: false }; }
const c2 = confirm('FINAL CONFIRM — Delete and restore now? Click OK to proceed.');
if (!c2) { log('User aborted'); return { ok: false }; }
// delete
log('Deleting existing DB WritingwayDB...');
await new Promise((res, rej) => {
const req = indexedDB.deleteDatabase('WritingwayDB');
req.onsuccess = () => { log('indexedDB.deleteDatabase success'); res(); };
req.onerror = (e) => { log('indexedDB.deleteDatabase error', e); rej(e); };
req.onblocked = () => { log('Delete blocked (close other tabs)'); };
});
// create and populate
log('Creating fresh DB and populating...');
await populateDexie('WritingwayDB', dump);
// attempt to set lastProject
if (dump.projects && dump.projects.length) {
try {
localStorage.setItem('writingway:lastProject', dump.projects[0].id);
log('Set writingway:lastProject to', dump.projects[0].id);
} catch (e) { log('Failed to set lastProject in localStorage', e); }
}
log('Restore complete. Please reload the main app page (e.g., open main.html or reload the existing tab).');
return { ok: true };
} catch (err) {
log('Restore failed', err && err.message ? err.message : err);
return { ok: false, error: err };
}
}
async function populateDexie(dbName, dump) {
// Use Dexie (loaded from CDN) to create a DB and bulk-put content
if (!window.Dexie) throw new Error('Dexie library not available');
const d = new Dexie(dbName);
// Define stores matching app schema (compatible)
d.version(1).stores({
projects: 'id, name, created, modified',
chapters: 'id, projectId, title, order, created, modified',
scenes: 'id, projectId, chapterId, title, order, created, modified',
content: 'sceneId, text, wordCount',
prompts: 'id, projectId, category, title, created, modified',
codex: 'id, projectId, title, created, modified',
compendium: 'id, projectId, category, title, modified, tags',
content_backups: 'id, sceneId, text, created'
});
await d.open();
// Use transaction to bulk put
await d.transaction('rw', d.projects, d.chapters, d.scenes, d.content, d.prompts, d.codex, d.compendium, d.content_backups, async () => {
if (dump.projects && dump.projects.length) await d.projects.bulkPut(dump.projects).catch(e => log('projects bulkPut err', e));
if (dump.chapters && dump.chapters.length) await d.chapters.bulkPut(dump.chapters).catch(e => log('chapters bulkPut err', e));
if (dump.scenes && dump.scenes.length) await d.scenes.bulkPut(dump.scenes).catch(e => log('scenes bulkPut err', e));
if (dump.content && dump.content.length) await d.content.bulkPut(dump.content).catch(e => log('content bulkPut err', e));
if (dump.prompts && dump.prompts.length) await d.prompts.bulkPut(dump.prompts).catch(e => log('prompts bulkPut err', e));
if (dump.codex && dump.codex.length) await d.codex.bulkPut(dump.codex).catch(e => log('codex bulkPut err', e));
if (dump.compendium && dump.compendium.length) await d.compendium.bulkPut(dump.compendium).catch(e => log('compendium bulkPut err', e));
if (dump.content_backups && dump.content_backups.length) await d.content_backups.bulkPut(dump.content_backups).catch(e => log('content_backups bulkPut err', e));
});
await d.close();
log('populateDexie finished for', dbName);
}
deleteRestoreEl.addEventListener('click', async () => {
deleteRestoreEl.disabled = true; deleteRestoreEl.textContent = 'Working...';
const res = await performRestore({ dbName: 'WritingwayDB', createBackup: false });
deleteRestoreEl.disabled = false; deleteRestoreEl.textContent = 'Delete DB and Restore';
if (res.ok) alert('Restore finished. Reload the main app page now.');
});
createBackupDbEl.addEventListener('click', async () => {
createBackupDbEl.disabled = true; createBackupDbEl.textContent = 'Working...';
const res = await performRestore({ createBackup: true, backupName: 'WritingwayDB_recovered' });
createBackupDbEl.disabled = false; createBackupDbEl.textContent = 'Create recovered DB (non-destructive)';
if (res.ok) alert('Backup DB created as WritingwayDB_recovered. You can open the app after changing DB name or inspect via DevTools.');
});
// Initialize
log('Ready. Paste JSON dump and click Validate to confirm.');
</script>
</body>
</html>