diff --git a/frontend/dashboard.css b/frontend/dashboard.css
index 5435ea8..cc4d601 100644
--- a/frontend/dashboard.css
+++ b/frontend/dashboard.css
@@ -14,6 +14,14 @@ header { position: sticky; top: 0; z-index: 20; padding: 12px 16px; display:flex
.live-data-status-header button, .live-data-status-actions button { min-height:44px; }
.issue-filing-receipt { position:fixed; inset:0; z-index:108; display:flex; align-items:flex-end; justify-content:center; background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
.issue-filing-receipt[hidden] { display:none; }
+.release-receipt-sheet { position:fixed; inset:0; width:100%; max-width:none; height:100%; max-height:none; margin:0; padding:0; border:0; background:rgba(5,12,21,.82); color:#e5e7eb; }
+.release-receipt-sheet::backdrop { background:rgba(5,12,21,.82); backdrop-filter:blur(4px); }
+.release-receipt-panel { position:absolute; left:0; right:0; bottom:0; box-sizing:border-box; width:min(560px,100%); max-height:100dvh; margin:auto; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
+.release-receipt-panel header { display:flex; align-items:flex-start; justify-content:space-between; gap:12px; }
+.release-receipt-launcher { width:100%; min-height:44px; border-color:#4ade80; }
+.release-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
+.release-receipt-panel button, .release-receipt-panel .button-link { box-sizing:border-box; display:flex; align-items:center; justify-content:center; min-width:0; min-height:44px; width:100%; text-align:center; }
+@media (max-width:359px) { .release-receipt-actions { grid-template-columns:1fr; } }
.issue-filing-receipt-panel { box-sizing:border-box; width:min(560px,100%); max-height:100dvh; overflow:auto; overflow-x:hidden; padding:18px; padding-bottom:calc(18px + env(safe-area-inset-bottom)); border:1px solid #4ade80; border-radius:18px 18px 0 0; background:#0b1526; overflow-wrap:anywhere; }
.issue-filing-receipt-panel h2, .issue-filing-receipt-panel h3 { margin:.25rem 0; }
.issue-filing-receipt-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; margin-top:16px; }
diff --git a/frontend/dashboard.js b/frontend/dashboard.js
index 22619ac..a4bb73f 100644
--- a/frontend/dashboard.js
+++ b/frontend/dashboard.js
@@ -334,6 +334,23 @@
let confirmedOwnerLogin = '';
let planningOwnerLogin = '';
let activeFlushLogin = '';
+ let releaseReceipt = null;
+ function attachReleaseReceipt() {
+ if (releaseReceipt) return releaseReceipt;
+ releaseReceipt = createReleaseReceipt({
+ storage:localStorage, getLogin:()=>confirmedOwnerLogin, fetchJson:fetchReviewJson,
+ launcher:qs('#release-receipt-launcher'), dialog:qs('#release-receipt-sheet'),
+ statusNode:qs('#release-receipt-status'), checksNode:qs('#release-receipt-checks'),
+ releaseNode:qs('#release-receipt-link'),
+ });
+ releaseReceipt.bind();
+ qs('#close-release-receipt').addEventListener('click', () => qs('#release-receipt-sheet').close());
+ qs('#refresh-release-receipt').addEventListener('click', () => releaseReceipt.refresh().catch(error => {
+ qs('#release-receipt-status').textContent = error.message + ' Retry when connected.';
+ }));
+ qs('#dismiss-release-receipt').addEventListener('click', () => releaseReceipt.dismiss());
+ return releaseReceipt;
+ }
const completedFiledReview = createCompletedFiledReview({
storage: localStorage,
getLogin() { return planningOwnerLogin; },
@@ -1136,6 +1153,7 @@
return false;
});
}
+ attachReleaseReceipt();
if (!reviewController) reviewController = createReviewController({ fetchJson: fetchReviewJson, storage: localStorage });
if (!wrapPreference) {
wrapPreference = createReviewController.createWrapPreference({
@@ -1145,6 +1163,14 @@
}
});
}
+ function restoreReleaseReceipt() {
+ const account = String(confirmedOwnerLogin || '').trim().toLowerCase();
+ if (!account) return;
+ try {
+ if (!localStorage.getItem('stackchain.release-receipt.v1:' + account)) return;
+ } catch (_error) { return; }
+ ensurePullWorkflow().then(() => releaseReceipt.restore()).catch(() => {});
+ }
const draftInbox = createDraftInbox({ storage: localStorage, getCurrentLogin: () => activeFlushLogin });
outboxCoordinator.subscribe(() => refreshMyWorkView());
const offlineWorkStore = createOfflineWorkStore({ storage: localStorage, indexedDB:window.indexedDB });
@@ -5246,6 +5272,7 @@
activeFlushLogin = contextIdentityFresh ? String(snapshot.context.user?.login || '').trim() : '';
if (activeFlushLogin) {
confirmedOwnerLogin = activeFlushLogin;
+ restoreReleaseReceipt();
updateDeliveryReceiptControls();
interruptionPrompt.restore();
}
@@ -7019,7 +7046,8 @@
button.disabled = true;
qs('#pull-sheet-status').textContent = 'Merging pull request…';
try {
- await pullController.merge(selectedPull, selectedPullDetail.head_sha);
+ const mergeResult = await pullController.merge(selectedPull, selectedPullDetail.head_sha);
+ releaseReceipt.capture(merging, mergeResult);
closePullSheet();
lastMyWork = lastMyWork.filter(item =>
!(item.kind === 'pull' && item.repository === merging.repository && item.number === merging.number)
@@ -7493,6 +7521,7 @@
if (!saved) return false;
const outage = mode === 'outage';
confirmedOwnerLogin = String(saved.user?.login || '').trim();
+ restoreReleaseReceipt();
planningOwnerLogin = confirmedOwnerLogin;
interruptionPrompt.restore();
updatePlanningAvailability();
diff --git a/frontend/index.html b/frontend/index.html
index fa3c9ae..0e5f48a 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -302,6 +302,7 @@
+
Today is saved on this device.
@@ -319,6 +320,15 @@
+