Compare commits

..

No commits in common. "475acbb92f2a612e61f7e55c5413cc7631a4e5b5" and "1bf759dc7699283d435da0d936a5301abea9290b" have entirely different histories.

12 changed files with 21 additions and 293 deletions

View File

@ -21,11 +21,7 @@ inspect/comment on assigned pull
requests, merge assigned pull requests, and submit pull-request reviews. requests, merge assigned pull requests, and submit pull-request reviews.
Pull-request replies and mobile My Work issue and PR comments use Gitea's Pull-request replies and mobile My Work issue and PR comments use Gitea's
issue-comment API; mobile issue capture requires issue issue-comment API; mobile issue capture requires issue
creation and assignment permission. Once a repository and meaningful title are selected, creation and assignment permission. The New issue sheet can optionally select an open
the New issue sheet checks for similar open issues in that repository. Candidate links keep
the draft intact; the first create attempt pauses until the operator reviews them or explicitly
chooses **Create anyway**. This check is advisory and never blocks offline capture or capture
when search is unavailable. The sheet can also optionally select an open
repository milestone and due date; the dashboard validates both and sends them with repository milestone and due date; the dashboard validates both and sends them with
self-assignment in the single create request, so planned work appears in its release self-assignment in the single create request, so planned work appears in its release
lane immediately. On a cold offline launch, **Save for filing** stores up to 20 lane immediately. On a cold offline launch, **Save for filing** stores up to 20

View File

@ -23,9 +23,6 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
const storageKey = 'stackchain.issue-capture.v1'; const storageKey = 'stackchain.issue-capture.v1';
const sharedStorageKey = 'stackchain.issue-share.v1'; const sharedStorageKey = 'stackchain.issue-share.v1';
let pending = null; let pending = null;
let duplicateRequest = 0;
let duplicateState = {status: 'idle', key: '', candidates: []};
let acknowledgedDuplicateKey = '';
const safeLabelIds = value => Array.from(new Set( const safeLabelIds = value => Array.from(new Set(
(Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0) (Array.isArray(value) ? value : []).filter(id => Number.isInteger(id) && id > 0)
)).slice(0, 20); )).slice(0, 20);
@ -143,51 +140,6 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
); );
} }
function duplicateKey(draft) {
const repository = String(draft?.repository || '').trim();
const title = String(draft?.title || '').replace(/\s+/g, ' ').trim();
return { repository, title, key: repository + '\n' + title.toLowerCase() };
}
async function findDuplicates(draft) {
const input = duplicateKey(draft);
const request = ++duplicateRequest;
if (!input.repository || input.title.length < 4) {
duplicateState = {status: 'idle', key: input.key, candidates: []};
return duplicateState;
}
try {
const payload = await fetchJson('api/v1/search?q=' + encodeURIComponent(input.title) + '&limit=10');
if (request !== duplicateRequest) return {status: 'stale', key: input.key, candidates: []};
duplicateState = {
status: 'ready',
key: input.key,
partial: payload?.partial === true,
candidates: (Array.isArray(payload?.items) ? payload.items : []).filter(item =>
item?.kind === 'issue' && item?.state === 'open' && item?.repository === input.repository
).slice(0, 3),
};
return duplicateState;
} catch (error) {
if (request !== duplicateRequest) return {status: 'stale', key: input.key, candidates: []};
duplicateState = {status: 'failed', key: input.key, candidates: [], error};
return duplicateState;
}
}
function needsDuplicateAcknowledgement(draft) {
const {key} = duplicateKey(draft);
return duplicateState.status === 'ready' && duplicateState.partial !== true && duplicateState.key === key &&
duplicateState.candidates.length > 0 && acknowledgedDuplicateKey !== key;
}
function acknowledgeDuplicates(draft) {
const {key} = duplicateKey(draft);
if (duplicateState.status === 'ready' && duplicateState.key === key && duplicateState.candidates.length) {
acknowledgedDuplicateKey = key;
}
}
function submit(draft) { function submit(draft) {
if (pending) return pending; if (pending) return pending;
const saved = saveDraft(draft); const saved = saveDraft(draft);
@ -214,8 +166,7 @@ function createIssueCapture({ fetchJson, storage, createOperationId = newIssueOp
} }
return { return {
saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, findDuplicates, saveDraft, loadDraft, clearDraft, loadLabels, loadMilestones, submit,
needsDuplicateAcknowledgement, acknowledgeDuplicates, submit,
stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent, stageSharedContent, pendingSharedContent, acceptSharedContent, discardSharedContent,
}; };
} }

View File

@ -274,12 +274,6 @@ textarea { resize: vertical; min-height: 120px; }
.create-issue-label-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:8px; } .create-issue-label-list { display:grid; grid-template-columns:repeat(auto-fit,minmax(140px,1fr)); gap:8px; }
.create-issue-label-option { min-height:44px; display:flex !important; grid-template-columns:auto 1fr !important; align-items:center; gap:8px; padding:8px 10px; border:1px solid #2a496e; border-radius:10px; background:#10213a; } .create-issue-label-option { min-height:44px; display:flex !important; grid-template-columns:auto 1fr !important; align-items:center; gap:8px; padding:8px 10px; border:1px solid #2a496e; border-radius:10px; background:#10213a; }
.create-issue-label-option input { width:20px; height:20px; margin:0; } .create-issue-label-option input { width:20px; height:20px; margin:0; }
.create-issue-duplicates { display:grid; gap:8px; min-width:0; padding:12px; border:1px solid #f59e0b; border-radius:10px; background:#2b210f; }
.create-issue-duplicates[hidden] { display:none; }
.create-issue-duplicate-list { display:grid; gap:8px; min-width:0; }
.create-issue-duplicate-card { display:grid; gap:4px; min-width:0; padding:10px; border:1px solid #6b5220; border-radius:10px; background:#171d29; overflow-wrap:anywhere; }
.create-issue-duplicate-card a, #create-issue-anyway { display:flex; align-items:center; min-height:44px; max-width:100%; }
.create-issue-duplicate-card a { color:#93c5fd; }
.create-issue-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; } .create-issue-actions { position:sticky; bottom:0; display:grid; gap:8px; padding:10px 0; padding-bottom:calc(10px + env(safe-area-inset-bottom)); background:#0b1526; }
.shared-content-conflict { display:grid; gap:8px; padding:12px; border:1px solid #8b5cf6; border-radius:10px; background:#16142b; } .shared-content-conflict { display:grid; gap:8px; padding:12px; border:1px solid #8b5cf6; border-radius:10px; background:#16142b; }
.shared-content-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; } .shared-content-actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; }

View File

@ -1717,67 +1717,6 @@
}); });
} }
function currentIssueCaptureDraft() {
return {
repository: qs('#create-issue-repository').value,
title: qs('#create-issue-title').value.trim(),
body: qs('#create-issue-body').value.trim(),
labelIds: selectedIssueLabelIds(),
milestoneId: Number(qs('#create-issue-milestone').value) || null,
dueDate: qs('#create-issue-due-date').value,
};
}
let duplicateCheckTimer = null;
function renderIssueDuplicates(state) {
if (state.status === 'stale') return;
const panel = qs('#create-issue-duplicates');
const list = qs('#create-issue-duplicate-list');
const status = qs('#create-issue-duplicate-status');
const anyway = qs('#create-issue-anyway');
list.replaceChildren();
anyway.hidden = true;
if (state.status === 'idle') {
panel.hidden = true;
return;
}
panel.hidden = false;
if (state.status === 'failed') {
status.textContent = 'Could not check for existing issues. You can still create this issue.';
return;
}
if (!state.candidates.length) {
status.textContent = 'No similar open issues found in this repository.';
return;
}
status.textContent = (state.partial ? 'Search was incomplete. ' : '') + state.candidates.length +
' similar open issue' + (state.candidates.length === 1 ? '' : 's') +
' found. ' + (state.partial ? 'You can continue without waiting.' : 'Review before creating another.');
state.candidates.forEach(item => {
const card = document.createElement('article');
card.className = 'create-issue-duplicate-card';
const key = document.createElement('span');
key.className = 'small';
key.textContent = item.repository + ' #' + item.number;
const link = document.createElement('a');
link.textContent = item.title || 'Review existing issue';
link.href = safeSearchUrl(item.url) || '#';
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.addEventListener('click', saveIssueCaptureDraft);
card.append(key, link);
list.appendChild(card);
});
}
function scheduleIssueDuplicateCheck() {
clearTimeout(duplicateCheckTimer);
const draft = currentIssueCaptureDraft();
duplicateCheckTimer = setTimeout(async () => {
renderIssueDuplicates(await issueCapture.findDuplicates(draft));
}, 350);
}
async function loadIssueLabels(repository, selectedIds = []) { async function loadIssueLabels(repository, selectedIds = []) {
const list = qs('#create-issue-label-list'); const list = qs('#create-issue-label-list');
const status = qs('#create-issue-label-status'); const status = qs('#create-issue-label-status');
@ -1863,7 +1802,6 @@
qs('#create-issue-due-date').value = captureDraft.dueDate || ''; qs('#create-issue-due-date').value = captureDraft.dueDate || '';
loadIssueLabels(qs('#create-issue-repository').value, captureDraft.labelIds); loadIssueLabels(qs('#create-issue-repository').value, captureDraft.labelIds);
loadIssueMilestones(qs('#create-issue-repository').value, captureDraft.milestoneId); loadIssueMilestones(qs('#create-issue-repository').value, captureDraft.milestoneId);
scheduleIssueDuplicateCheck();
qs('#create-issue-status').textContent = repositories.length ? '' : 'No accessible repositories are available.'; qs('#create-issue-status').textContent = repositories.length ? '' : 'No accessible repositories are available.';
qs('#submit-new-issue').disabled = !repositories.length; qs('#submit-new-issue').disabled = !repositories.length;
qs('#create-issue-sheet').classList.add('open'); qs('#create-issue-sheet').classList.add('open');
@ -1879,8 +1817,6 @@
return; return;
} }
qs('#create-issue-sheet').classList.remove('open'); qs('#create-issue-sheet').classList.remove('open');
clearTimeout(duplicateCheckTimer);
qs('#create-issue-duplicates').hidden = true;
creatingIssue = false; creatingIssue = false;
qs('#new-issue').focus(); qs('#new-issue').focus();
} }
@ -2629,39 +2565,30 @@
closeCreateIssueSheet(true, !discardEditedDraft); closeCreateIssueSheet(true, !discardEditedDraft);
}); });
['#create-issue-title', '#create-issue-body', '#create-issue-due-date'].forEach(selector => ['#create-issue-title', '#create-issue-body', '#create-issue-due-date'].forEach(selector =>
qs(selector).addEventListener('input', () => { qs(selector).addEventListener('input', saveIssueCaptureDraft)
saveIssueCaptureDraft();
if (selector === '#create-issue-title') scheduleIssueDuplicateCheck();
})
); );
qs('#create-issue-repository').addEventListener('change', event => { qs('#create-issue-repository').addEventListener('change', event => {
loadIssueLabels(event.target.value); loadIssueLabels(event.target.value);
loadIssueMilestones(event.target.value); loadIssueMilestones(event.target.value);
saveIssueCaptureDraft(); saveIssueCaptureDraft();
scheduleIssueDuplicateCheck();
}); });
qs('#create-issue-label-list').addEventListener('change', saveIssueCaptureDraft); qs('#create-issue-label-list').addEventListener('change', saveIssueCaptureDraft);
qs('#create-issue-milestone').addEventListener('change', saveIssueCaptureDraft); qs('#create-issue-milestone').addEventListener('change', saveIssueCaptureDraft);
qs('#create-issue-anyway').addEventListener('click', () => {
const captureDraft = currentIssueCaptureDraft();
issueCapture.acknowledgeDuplicates(captureDraft);
qs('#create-issue-anyway').hidden = true;
qs('#create-issue-form').requestSubmit();
});
qs('#create-issue-form').addEventListener('submit', async event => { qs('#create-issue-form').addEventListener('submit', async event => {
event.preventDefault(); event.preventDefault();
const captureDraft = currentIssueCaptureDraft(); const captureDraft = {
repository: qs('#create-issue-repository').value,
title: qs('#create-issue-title').value.trim(),
body: qs('#create-issue-body').value.trim(),
labelIds: selectedIssueLabelIds(),
milestoneId: Number(qs('#create-issue-milestone').value) || null,
dueDate: qs('#create-issue-due-date').value,
};
if (!captureDraft.repository || !captureDraft.title) { if (!captureDraft.repository || !captureDraft.title) {
qs('#create-issue-status').textContent = 'Choose a repository and add a title.'; qs('#create-issue-status').textContent = 'Choose a repository and add a title.';
qs('#create-issue-title').focus(); qs('#create-issue-title').focus();
return; return;
} }
if (issueCapture.needsDuplicateAcknowledgement(captureDraft)) {
qs('#create-issue-status').textContent = 'Review the possible existing issues, or choose Create anyway.';
qs('#create-issue-anyway').hidden = false;
qs('#create-issue-anyway').focus();
return;
}
const button = qs('#submit-new-issue'); const button = qs('#submit-new-issue');
button.disabled = true; button.disabled = true;
qs('#create-issue-status').textContent = 'Saving for background delivery…'; qs('#create-issue-status').textContent = 'Saving for background delivery…';

View File

@ -355,12 +355,6 @@
<div class="small" id="create-issue-label-status" aria-live="polite">Choose a repository to load labels.</div> <div class="small" id="create-issue-label-status" aria-live="polite">Choose a repository to load labels.</div>
<div class="create-issue-label-list" id="create-issue-label-list"></div> <div class="create-issue-label-list" id="create-issue-label-list"></div>
</fieldset> </fieldset>
<section class="create-issue-duplicates" id="create-issue-duplicates" aria-live="polite" hidden>
<strong>Possible existing issues</strong>
<div id="create-issue-duplicate-status" class="small"></div>
<div id="create-issue-duplicate-list" class="create-issue-duplicate-list"></div>
<button id="create-issue-anyway" type="button" hidden>Create anyway</button>
</section>
<div class="small">The issue will be assigned to you.</div> <div class="small">The issue will be assigned to you.</div>
<div class="create-issue-actions"> <div class="create-issue-actions">
<button id="save-unfiled-issue" type="button">Save for filing</button> <button id="save-unfiled-issue" type="button">Save for filing</button>

View File

@ -1,6 +1,6 @@
const BASE = new URL('./', self.location.href).pathname; const BASE = new URL('./', self.location.href).pathname;
importScripts(BASE + 'static/background-issue-sync.js'); importScripts(BASE + 'static/background-issue-sync.js');
const CACHE = 'stackchain-dashboard-shell-v52'; const CACHE = 'stackchain-dashboard-shell-v51';
const OUTAGE_STATUSES = new Set([500, 502, 503, 504]); const OUTAGE_STATUSES = new Set([500, 502, 503, 504]);
const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000; const NAVIGATION_TIMEOUT_MS = self.__STACKCHAIN_NAVIGATION_TIMEOUT_MS || 4000;
const SHELL = [ const SHELL = [

View File

@ -233,5 +233,5 @@ async def test_dashboard_syncs_every_later_change_and_exposes_account_status():
def test_later_sync_ships_atomically_in_the_offline_shell(): def test_later_sync_ships_atomically_in_the_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text() source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/later-sync.js'" in source assert "BASE + 'static/later-sync.js'" in source

View File

@ -137,4 +137,4 @@ def test_markdown_work_bodies_are_mobile_safe_block_containers():
assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css assert ".markdown-content { min-width:0; max-width:100%; overflow-wrap:anywhere;" in css
assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css assert ".markdown-content pre { max-width:100%; overflow-x:auto;" in css
assert ".markdown-content a { min-height:44px;" in css assert ".markdown-content a { min-height:44px;" in css
assert "stackchain-dashboard-shell-v52" in worker assert "stackchain-dashboard-shell-v51" in worker

View File

@ -35,4 +35,4 @@ def test_offline_shell_contains_every_local_dashboard_runtime_asset():
shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0])) shell_assets = set(re.findall(r"BASE \+ '([^']+)'", worker.split("async function sessionCsrf", 1)[0]))
assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}" assert local_assets <= shell_assets, f"Offline shell is missing: {sorted(local_assets - shell_assets)}"
assert "stackchain-dashboard-shell-v52" in worker assert "stackchain-dashboard-shell-v51" in worker

View File

@ -1966,113 +1966,6 @@ Promise.all([capture.loadMilestones('stackchain/api'), capture.submit(capture.lo
assert output["stored"]["number"] == 221 assert output["stored"]["number"] == 221
def test_issue_capture_finds_only_open_issue_duplicates_in_the_selected_repository():
script = f"""
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
const calls = [];
const capture = createIssueCapture({{
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
fetchJson: url => {{
calls.push(url);
return Promise.resolve({{items:[
{{kind:'issue', state:'open', repository:'stackchain/api', number:7, title:'Investigate checkout latency'}},
{{kind:'pull', state:'open', repository:'stackchain/api', number:8, title:'Investigate checkout latency'}},
{{kind:'issue', state:'closed', repository:'stackchain/api', number:9, title:'Investigate checkout latency'}},
{{kind:'issue', state:'open', repository:'stackchain/web', number:10, title:'Investigate checkout latency'}},
{{kind:'issue', state:'open', repository:'stackchain/api', number:11, title:'Checkout latency alert'}},
{{kind:'issue', state:'open', repository:'stackchain/api', number:12, title:'Checkout latency follow-up'}},
{{kind:'issue', state:'open', repository:'stackchain/api', number:13, title:'Fourth match is bounded'}},
]}});
}},
}});
capture.findDuplicates({{repository:'stackchain/api', title:' Investigate checkout latency '}}).then(state =>
process.stdout.write(JSON.stringify({{calls, state}}))
);
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["calls"] == [
"api/v1/search?q=Investigate%20checkout%20latency&limit=10"
]
assert output["state"]["status"] == "ready"
assert [item["number"] for item in output["state"]["candidates"]] == [7, 11, 12]
def test_issue_capture_ignores_stale_duplicate_results_and_resets_create_anyway_acknowledgement():
script = f"""
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
const pending = [];
const capture = createIssueCapture({{
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
fetchJson: url => new Promise((resolve, reject) => pending.push({{url, resolve, reject}})),
}});
const firstDraft = {{repository:'stackchain/api', title:'Checkout latency'}};
const latestDraft = {{repository:'stackchain/api', title:'Checkout latency alert'}};
const first = capture.findDuplicates(firstDraft);
const latest = capture.findDuplicates(latestDraft);
pending[1].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:22,title:'Checkout latency alert'}}]}});
latest.then(latestState => {{
const blocked = capture.needsDuplicateAcknowledgement(latestDraft);
capture.acknowledgeDuplicates(latestDraft);
const allowed = capture.needsDuplicateAcknowledgement(latestDraft);
const changedDraft = {{...latestDraft,title:'Checkout latency alert today'}};
const changedSearch = capture.findDuplicates(changedDraft);
pending[2].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:23,title:'Checkout latency alert today'}}]}});
changedSearch.then(() => {{
const changed = capture.needsDuplicateAcknowledgement(changedDraft);
pending[0].resolve({{items:[{{kind:'issue',state:'open',repository:'stackchain/api',number:21,title:'Old result'}}]}});
first.then(staleState => {{
const failure = capture.findDuplicates({{repository:'stackchain/api',title:'Network failure'}});
pending[3].reject(new Error('offline'));
failure.then(failedState => process.stdout.write(JSON.stringify({{
latestState, staleState, blocked, allowed, changed, failedState,
failureBlocks:capture.needsDuplicateAcknowledgement({{repository:'stackchain/api',title:'Network failure'}}),
}})));
}});
}});
}});
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["latestState"]["status"] == "ready"
assert output["staleState"]["status"] == "stale"
assert output["blocked"] is True
assert output["allowed"] is False
assert output["changed"] is True
assert output["failedState"]["status"] == "failed"
assert output["failureBlocks"] is False
def test_issue_capture_keeps_partial_duplicate_search_advisory():
script = f"""
const createIssueCapture = require({json.dumps(str(CREATE_ISSUE_SHEET))});
const draft = {{repository:'stackchain/api', title:'Checkout latency'}};
const capture = createIssueCapture({{
storage: {{getItem:()=>null, setItem:()=>{{}}, removeItem:()=>{{}}}},
fetchJson: () => Promise.resolve({{partial:true, items:[
{{kind:'issue',state:'open',repository:'stackchain/api',number:24,title:'Checkout latency'}},
]}}),
}});
capture.findDuplicates(draft).then(state => process.stdout.write(JSON.stringify({{
state, blocked:capture.needsDuplicateAcknowledgement(draft),
}})));
"""
result = subprocess.run(
["node", "-e", script], check=True, capture_output=True, text=True
)
output = json.loads(result.stdout)
assert output["state"]["partial"] is True
assert [item["number"] for item in output["state"]["candidates"]] == [24]
assert output["blocked"] is False
def test_unread_updates_enrich_matching_work_and_keep_unassigned_mentions_actionable(): def test_unread_updates_enrich_matching_work_and_keep_unassigned_mentions_actionable():
payload = { payload = {
"user": {"login": "timmy"}, "user": {"login": "timmy"},
@ -3255,25 +3148,6 @@ async def test_mobile_my_work_captures_new_issue_in_accessible_draft_safe_sheet(
assert '.create-issue-label-option' in html and 'min-height:44px' in html assert '.create-issue-label-option' in html and 'min-height:44px' in html
@pytest.mark.anyio
async def test_mobile_issue_capture_warns_before_queuing_a_possible_duplicate():
html = await dashboard()
assert 'id="create-issue-duplicates"' in html
assert 'id="create-issue-duplicate-list"' in html
assert 'aria-live="polite"' in html
assert 'id="create-issue-anyway"' in html and 'Create anyway' in html
assert "issueCapture.findDuplicates(draft)" in html
assert "issueCapture.needsDuplicateAcknowledgement(captureDraft)" in html
assert "issueCapture.acknowledgeDuplicates(captureDraft)" in html
assert html.index("issueCapture.needsDuplicateAcknowledgement(captureDraft)") < html.index(
"issueOutbox.enqueueDurably(captureDraft)"
)
assert ".create-issue-duplicate-card" in html
assert ".create-issue-duplicate-card a" in html and "min-height:44px" in html
assert "overflow-wrap:anywhere" in html
@pytest.mark.anyio @pytest.mark.anyio
async def test_mobile_dashboard_puts_filterable_my_work_before_auxiliary_panels(): async def test_mobile_dashboard_puts_filterable_my_work_before_auxiliary_panels():
html = await dashboard() html = await dashboard()

View File

@ -105,25 +105,17 @@ async function dispatchNotificationClick(route) {{
return json.loads(completed.stdout) return json.loads(completed.stdout)
def test_duplicate_aware_capture_ships_in_a_new_offline_shell():
source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source
assert "BASE + 'static/create-issue-sheet.js'" in source
assert "BASE + 'static/dashboard.js'" in source
def test_exact_later_picker_ships_atomically_in_a_new_offline_shell(): def test_exact_later_picker_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text() source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/later-picker.js'" in source assert "BASE + 'static/later-picker.js'" in source
def test_navigation_deadline_ships_in_a_new_shell_cache(): def test_navigation_deadline_ships_in_a_new_shell_cache():
source = WORKER.read_text() source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/dashboard.css'" in source assert "BASE + 'static/dashboard.css'" in source
assert "BASE + 'static/dashboard.js'" in source assert "BASE + 'static/dashboard.js'" in source
assert "BASE + 'static/install-app.js'" in source assert "BASE + 'static/install-app.js'" in source
@ -132,21 +124,21 @@ def test_navigation_deadline_ships_in_a_new_shell_cache():
def test_today_convergence_ships_in_a_new_shell_cache(): def test_today_convergence_ships_in_a_new_shell_cache():
source = WORKER.read_text() source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/today-sync.js'" in source assert "BASE + 'static/today-sync.js'" in source
def test_mobile_search_viewport_ships_in_a_new_offline_shell(): def test_mobile_search_viewport_ships_in_a_new_offline_shell():
source = WORKER.read_text() source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/mobile-search-viewport.js'" in source assert "BASE + 'static/mobile-search-viewport.js'" in source
def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell(): def test_update_ownership_flow_ships_atomically_in_a_new_offline_shell():
source = WORKER.read_text() source = WORKER.read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/update-ownership.js'" in source assert "BASE + 'static/update-ownership.js'" in source

View File

@ -63,7 +63,7 @@ sync.enqueue('add', 'issue:r:1:');
def test_inflight_today_drain_ships_in_a_new_offline_shell(): def test_inflight_today_drain_ships_in_a_new_offline_shell():
source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text() source = (Path(__file__).parents[1] / "frontend" / "service-worker.js").read_text()
assert "stackchain-dashboard-shell-v52" in source assert "stackchain-dashboard-shell-v51" in source
assert "BASE + 'static/today-sync.js'" in source assert "BASE + 'static/today-sync.js'" in source