diff --git a/src/index.html b/src/index.html
index 8fbb634..86dd97c 100644
--- a/src/index.html
+++ b/src/index.html
@@ -8389,6 +8389,48 @@
return { attempted: legacyItems.length, migrated: migratedItems.length, failed };
}
+ async function repairPremiumPurchases({ silent = false } = {}) {
+ if (!_supportsVault() || !state.community?.myPubkey) {
+ return { status: 'unavailable', pending: null, migrated: null, checked: 0, ok: 0, missing: 0, unrecoverable: 0 };
+ }
+ const pending = await retryPendingVaultWrites({ silent: true });
+ const migrated = await migrateLegacyVaultPrivateCopies({ silent: true });
+ const vault = await loadPurchaseVault({ force: true });
+ let checked = 0;
+ let ok = 0;
+ let missing = 0;
+ let unrecoverable = 0;
+ for (const item of vault?.items || []) {
+ if (item.privateCopy) {
+ checked++;
+ try {
+ await _loadPurchaseCopyPayload(item.privateCopy);
+ ok++;
+ } catch (err) {
+ missing++;
+ if (!item.payload) unrecoverable++;
+ console.warn('[vault] repair private-copy validation failed', item.privateCopy, err);
+ }
+ } else if (item.payload) {
+ missing++;
+ } else {
+ unrecoverable++;
+ }
+ }
+ const status = pending.failed || migrated.failed || unrecoverable ? 'needs_attention' : missing ? 'repairable' : 'clean';
+ const result = { status, pending, migrated, checked, ok, missing, unrecoverable };
+ if (!silent) {
+ if (status === 'clean') {
+ toastSuccess('Purchase repair found everything clean ✓');
+ } else if (status === 'repairable') {
+ toast('Purchase repair found legacy purchases that can still be saved as private copies.');
+ } else {
+ toastError(`Purchase repair needs attention: ${unrecoverable} unrecoverable, ${pending.failed + migrated.failed} failed retry/migration.`);
+ }
+ }
+ return result;
+ }
+
function _buildVaultItemForUnlock({ row, totalSats, creatorSats, platformSats, creatorPreimage, platformPreimage, payload }) {
return {
a: row.premium?.address || '',
@@ -9349,6 +9391,23 @@
});
actions.appendChild(migrateBtn);
}
+ const repairBtn = document.createElement('button');
+ repairBtn.type = 'button';
+ repairBtn.textContent = 'Repair purchases';
+ repairBtn.title = 'Check private copies, retry pending saves, and migrate recoverable legacy purchases.';
+ repairBtn.addEventListener('click', async () => {
+ repairBtn.disabled = true;
+ repairBtn.textContent = 'Repairing…';
+ try {
+ const result = await repairPremiumPurchases({ silent: false });
+ await loadPurchasedDesignsIntoWall(wall, { force: true });
+ console.info('[vault] repair result', result);
+ } finally {
+ repairBtn.disabled = false;
+ repairBtn.textContent = 'Repair purchases';
+ }
+ });
+ actions.appendChild(repairBtn);
const refreshBtn = document.createElement('button');
refreshBtn.type = 'button';
refreshBtn.textContent = '↻ Refresh';
diff --git a/tests/premium/premium2-index-integration.test.mjs b/tests/premium/premium2-index-integration.test.mjs
index 1580e05..58edf70 100644
--- a/tests/premium/premium2-index-integration.test.mjs
+++ b/tests/premium/premium2-index-integration.test.mjs
@@ -89,6 +89,19 @@ test('index page migrates legacy inline vault purchases to private copies', asyn
assert.match(html, /await migrateLegacyVaultPrivateCopies\(\{ silent: false \}\)/);
});
+test('index page exposes Premium 2.0 purchase repair workflow', async () => {
+ const html = await indexHtml();
+
+ assert.match(html, /async function repairPremiumPurchases/);
+ assert.match(html, /const pending = await retryPendingVaultWrites\(\{ silent: true \}\)/);
+ assert.match(html, /const migrated = await migrateLegacyVaultPrivateCopies\(\{ silent: true \}\)/);
+ assert.match(html, /await _loadPurchaseCopyPayload\(item\.privateCopy\)/);
+ assert.match(html, /status = pending\.failed \|\| migrated\.failed \|\| unrecoverable \? 'needs_attention' : missing \? 'repairable' : 'clean'/);
+ assert.match(html, /repairBtn\.textContent = 'Repair purchases'/);
+ assert.match(html, /const result = await repairPremiumPurchases\(\{ silent: false \}\)/);
+ assert.match(html, /Purchase repair found everything clean/);
+});
+
test('vault item stubs preserve Premium 2.0 private-copy fields', async () => {
const html = await indexHtml();
const stubStart = html.indexOf('function _vaultItemStub(item)');