Skip to content

Commit a3b4c0f

Browse files
committed
feat(recovery): show bundle verification status when below threshold (#147)
When a friend opens their personalized recover.html with only their own share loaded (below threshold), the recovery tool now shows a verification card confirming their bundle is valid. The card displays: - "Your bundle is working" with a checkmark - Which piece they hold (e.g. "piece 1 of 3") - How many pieces are needed for recovery - Whether the encrypted archive is pre-loaded - A reminder to keep the bundle safe The card appears automatically when the personalized share loads and disappears once enough pieces are gathered for recovery. Friends may open their bundle months or years after receiving it to check if everything is still functional. Without a verification signal, they have no way to confirm their piece will work when recovery is actually needed. The existing below-threshold state just shows "waiting for more shares" with no validation feedback. This is especially relevant for the non-technical users rememory is designed for. They need reassurance that their responsibility (keeping the bundle safe) has been met. - Built the binary with changes, sealed a 2-of-3 test project - Extracted Alice's bundle and verified the verification-status div, CSS, and translation keys are all embedded in recover.html - Opened recover.html in browser and confirmed the verification card appears with correct piece index, threshold, and manifest status - Verified the card disappears when a second share is added (threshold met) - Verified manifest status line updates when manifest is loaded/cleared ![Verification card in recover.html](https://vhs.charm.sh/vhs-4vq1N9ptnbmTJdviH0UjGG.gif) <img width="1708" height="896" alt="CleanShot 2026-04-05 at 15 48 29@2x" src="https://github.com/user-attachments/assets/b9e99c40-dc4f-493d-95da-e61a937cf68d" /> - [x] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) and this PR follows the guidelines - [x] A human has reviewed the **entire diff** of this PR, every line of code - [x] A human understands the changes and can explain why this approach is correct - [x] Tests pass (`make full`) - [x] This PR doesn't have AI-generated boilerplate or co-author lines - [ ] This PR was authored and submitted by an AI agent without human review This contribution was developed with AI assistance.
2 parents 7991277 + 41353e0 commit a3b4c0f

4 files changed

Lines changed: 88 additions & 1 deletion

File tree

internal/html/assets/recover.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ <h2><span class="step-number">1</span> <span data-i18n="step1_title">Gather the
2828

2929
<div id="shares-list" class="shares-list"></div>
3030

31+
<div id="verification-status" class="verification-status hidden"></div>
32+
3133
<!-- Contact list for other friends (populated via JS if personalization data exists) -->
3234
<div id="contact-list-section" class="contact-list-section hidden">
3335
<h3 data-i18n="contact_list">Contact the others</h3>

internal/html/assets/src/app.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ type UIShare = ParsedShare & { isHolder?: boolean };
114114
pasteSubmitBtn: HTMLButtonElement | null;
115115
contactListSection: HTMLElement | null;
116116
contactList: HTMLElement | null;
117+
verificationStatus: HTMLElement | null;
117118
step1Card: HTMLElement | null;
118119
step2Card: HTMLElement | null;
119120
scanQrBtn: HTMLButtonElement | null;
@@ -146,6 +147,7 @@ type UIShare = ParsedShare & { isHolder?: boolean };
146147
pasteSubmitBtn: document.getElementById('paste-submit-btn') as HTMLButtonElement | null,
147148
contactListSection: document.getElementById('contact-list-section'),
148149
contactList: document.getElementById('contact-list'),
150+
verificationStatus: document.getElementById('verification-status'),
149151
step1Card: null,
150152
step2Card: null,
151153
scanQrBtn: document.getElementById('scan-qr-btn') as HTMLButtonElement | null,
@@ -1088,6 +1090,46 @@ type UIShare = ParsedShare & { isHolder?: boolean };
10881090
}
10891091

10901092
updateContactList();
1093+
updateVerificationStatus();
1094+
}
1095+
1096+
// ============================================
1097+
// Bundle Verification Status
1098+
// ============================================
1099+
1100+
function updateVerificationStatus(): void {
1101+
if (!elements.verificationStatus) return;
1102+
1103+
// Show verification when we have shares but haven't reached threshold yet
1104+
const hasShares = state.shares.length > 0;
1105+
const belowThreshold = state.threshold > 0 && state.shares.length < state.threshold;
1106+
const notRecovering = !state.recovering && !state.recoveryComplete;
1107+
1108+
if (hasShares && belowThreshold && notRecovering) {
1109+
const share = state.shares[0];
1110+
const manifestPresent = state.manifest !== null;
1111+
1112+
const manifestLine = manifestPresent
1113+
? t('verification_manifest_ok')
1114+
: t('verification_manifest_missing');
1115+
1116+
elements.verificationStatus.innerHTML = `
1117+
<div class="verification-header">
1118+
<span>&#9989;</span> ${escapeHtml(t('verification_title'))}
1119+
</div>
1120+
<div class="verification-details">
1121+
<p>${escapeHtml(t('verification_piece', share.index, share.total))}</p>
1122+
<p>${escapeHtml(t('verification_threshold', state.threshold))}</p>
1123+
<p>${escapeHtml(manifestLine)}</p>
1124+
</div>
1125+
<div class="verification-note">
1126+
${escapeHtml(t('verification_keep_safe'))}
1127+
</div>
1128+
`;
1129+
elements.verificationStatus.classList.remove('hidden');
1130+
} else {
1131+
elements.verificationStatus.classList.add('hidden');
1132+
}
10911133
}
10921134

10931135
// ============================================
@@ -1120,6 +1162,8 @@ type UIShare = ParsedShare & { isHolder?: boolean };
11201162
const clearBtn = elements.manifestStatus.querySelector('.clear-manifest');
11211163
clearBtn?.addEventListener('click', clearManifest);
11221164
}
1165+
1166+
updateVerificationStatus();
11231167
}
11241168

11251169
function clearManifest(): void {
@@ -1129,6 +1173,7 @@ type UIShare = ParsedShare & { isHolder?: boolean };
11291173
elements.manifestStatus?.classList.remove('loaded');
11301174
elements.manifestDropZone?.classList.remove('hidden');
11311175
checkRecoverReady();
1176+
updateVerificationStatus();
11321177
}
11331178

11341179
// ============================================

internal/html/assets/styles.css

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,40 @@ input[type="file"] {
221221
font-size: 1rem;
222222
}
223223

224+
.verification-status {
225+
margin-top: 1rem;
226+
padding: 1rem 1.25rem;
227+
background: var(--success-bg);
228+
border: 1px solid var(--success-border);
229+
border-radius: 6px;
230+
color: var(--success-text);
231+
line-height: 1.5;
232+
}
233+
234+
.verification-status .verification-header {
235+
display: flex;
236+
align-items: center;
237+
gap: 0.5rem;
238+
font-weight: 600;
239+
margin-bottom: 0.5rem;
240+
}
241+
242+
.verification-status .verification-details {
243+
font-size: 0.9rem;
244+
color: var(--text-secondary);
245+
}
246+
247+
.verification-status .verification-details p {
248+
margin: 0.25rem 0;
249+
}
250+
251+
.verification-status .verification-note {
252+
margin-top: 0.75rem;
253+
font-size: 0.85rem;
254+
color: var(--text-muted);
255+
font-style: italic;
256+
}
257+
224258
.threshold-info {
225259
padding: 0.75rem 1rem;
226260
background: var(--sage-tint);

internal/translations/recover/en.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,5 +92,11 @@
9292
"tlock_error_message": "Your pieces worked, but the time lock couldn't be verified. This usually means the network request was blocked.",
9393
"tlock_error_guidance": "Try a different browser (Chrome works best), or download the locked archive and use the CLI tool.",
9494
"tlock_error_status": "Time lock check failed. Your pieces are correct — try a different browser.",
95-
"tlock_download_archive": "Download locked archive"
95+
"tlock_download_archive": "Download locked archive",
96+
"verification_title": "Your bundle is working",
97+
"verification_piece": "You hold piece {0} of {1}.",
98+
"verification_threshold": "At least {0} pieces are needed for recovery.",
99+
"verification_manifest_ok": "Encrypted archive is pre-loaded.",
100+
"verification_manifest_missing": "The encrypted archive will need to be provided during recovery.",
101+
"verification_keep_safe": "Keep this bundle somewhere safe. When the time comes, you and the others will combine your pieces to recover."
96102
}

0 commit comments

Comments
 (0)