From 37b179a10da37f96123b1e6b7669c3e8513cef5a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 11 Sep 2026 01:48:55 +0000 Subject: [PATCH] =?UTF-8?q?board:=20one-tap=20mode=20=E2=80=94=20file=20th?= =?UTF-8?q?e=20issue=20from=20the=20page,=20with=20inline=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every action button opened a prefilled GitHub issue that still had to be submitted there: two taps, and the second one left the dashboard. With a fine-grained token pasted once into a ๐Ÿ”‘ chip (Issues read/write on this repo, kept in that browser's local storage), a tap now creates the same issue from the page through the GitHub API โ€” title, body and label read back out of the link the button already carries, so the workflows behind it see no difference and the links stay the fallback without a token. The row disappears at once and stays hidden across reloads until the board's own re-render no longer carries it (or a day passes, which means the action failed and it comes back). ๐Ÿ“ฅ/๐Ÿ“‘ open an inline notes box first, and the notes replace the placeholder line in the issue body. A failed call leaves the row and says why in a toast. Verified in headless Chromium against a stubbed API: setup, add, intake with notes, hidden-across-reload, failure, sign-out. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Rhn9qWAjP54dP27dyEibG2 --- README.md | 9 ++++ scripts/board.py | 127 ++++++++++++++++++++++++++++++++++++++++++-- tests/test_board.py | 45 ++++++++++++++-- 3 files changed, 174 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cc6dd78..73bbbc9 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,15 @@ paper links to its arXiv abstract page and carries a ๐Ÿ“„ button onto the PDF itself, so a phone can collect a stack of papers to read offline in one tap each. +**One-tap mode.** Out of the box each action button opens a prefilled GitHub +issue you then submit. Tap the ๐Ÿ”‘ chip under the header once and paste a +fine-grained token (Resource owner PyAutoLabs, repository PyAutoMemory only, +*Issues: read and write*): from then on every button files that same issue +from the page, the row disappears at once, and ๐Ÿ“ฅ/๐Ÿ“‘ ask for your notes in an +inline box first. The token lives in that browser's local storage and nowhere +else; tap the chip again to sign out. The workflows behind the buttons see +exactly the issue the link would have opened. + The per-paper buttons file GitHub issues that two workflows act on: `queue_actions.yml` makes the mechanical moves (โž• โœ… โœ–๏ธ ๐Ÿงน) and closes the issue; `queue_filing.yml` has Claude file a ๐Ÿ“ฅ/๐Ÿ“‘ paper onto a diff --git a/scripts/board.py b/scripts/board.py index 6047954..ddc0c2b 100644 --- a/scripts/board.py +++ b/scripts/board.py @@ -910,6 +910,27 @@ def _bar(statuses: dict) -> str: .fresh.stale{color:var(--warn);font-weight:600} table.recent td.name{white-space:nowrap} footer{margin-top:2rem;color:var(--muted);font-size:.82em} +/* One-tap mode (see the script): the ๐Ÿ”‘ chip, the inline notes box a ๐Ÿ“ฅ/๐Ÿ“‘ + tap opens, a busy button, and the toast that says what just happened. */ +#onetap{font:inherit;font-size:.9em;padding:.1rem .5rem;border:1px solid var(--line); + border-radius:6px;background:var(--btn);color:var(--fg);cursor:pointer} +#onetap:hover{border-color:var(--accent)} +#onetap.on{border-color:var(--ok);color:var(--ok)} +.notes{margin:.4rem 0 .2rem;padding:.5rem;border:1px solid var(--line); + border-radius:8px;background:var(--btn)} +.notes textarea{width:100%;box-sizing:border-box;font:inherit;padding:.4rem; + border:1px solid var(--line);border-radius:6px;background:var(--bg);color:var(--fg)} +.notes textarea:focus{outline:none;border-color:var(--accent)} +.notes button{margin-top:.35rem;font:inherit;padding:.25rem .6rem; + border:1px solid var(--line);border-radius:6px;background:var(--bg); + color:var(--fg);cursor:pointer} +.notes button.go{border-color:var(--accent);font-weight:600} +a.act.busy{opacity:.5;pointer-events:none} +#toast{position:fixed;left:50%;bottom:1.2rem;transform:translateX(-50%); + max-width:90vw;padding:.5rem .9rem;border-radius:8px;background:var(--fg); + color:var(--bg);opacity:0;transition:opacity .2s;pointer-events:none;z-index:9} +#toast.show{opacity:1} +#toast.bad{background:var(--warn)} """ # The shared copy handler is delegated, so a chip inside a would @@ -923,7 +944,7 @@ def _bar(statuses: dict) -> str: # baked in at render time would sit at "0 weekdays" forever and never fire in # the one failure it exists for. The rendered date is a fact and survives the # freeze; the verdict is recomputed against the reader's own clock. -_EXTRA_JS = """ +_EXTRA_JS = r""" function freshness(){ var els=document.querySelectorAll('.fresh[data-last-digest]'); for(var i=0;i str: else{d.style.display='';d.open=openByDefault;} var h=d.querySelector('details.hist'); if(h){h.style.display=(q&&!histHit)?'none':'';h.open=!!q&&histHit;}}} +/* --- one-tap mode ---------------------------------------------------------- + Every action button is a prefilled new-issue link, and that stays the + fallback. With a fine-grained token pasted into the ๐Ÿ”‘ chip (stored in this + browser only), a tap creates that same issue from here through the GitHub + API instead of leaving the page: same title, body and label, read back out + of the link itself, so the workflows behind it see no difference. The row + disappears at once and stays hidden across reloads until the re-render no + longer carries it (or a day passes with it still there, which means the + action failed and it comes back). ๐Ÿ“ฅ/๐Ÿ“‘ open an inline notes box first โ€” + the notes are the one thing the link could not carry filled in. */ +var TOK='memory-board-token',HID='memory-board-hidden',API='https://api.github.com'; +function repo(){return document.body.getAttribute('data-repo')||'';} +function token(){try{return localStorage.getItem(TOK)||'';}catch(e){return '';}} +function hiddenMap(){try{return JSON.parse(localStorage.getItem(HID)||'{}');}catch(e){return {};}} +function saveHidden(h){try{localStorage.setItem(HID,JSON.stringify(h));}catch(e){}} +function toast(msg,bad){var t=document.getElementById('toast');if(!t)return; + t.textContent=msg;t.className=bad?'show bad':'show'; + clearTimeout(toast.h);toast.h=setTimeout(function(){t.className='';},bad?7000:4000);} +function request(a){var u;try{u=new URL(a.href);}catch(e){return null;} + if(u.pathname.indexOf('/issues/new')<0)return null; + var body=u.searchParams.get('body')||''; + var m=body.match(/^(?:line|date): (.*)$/m); + return {title:u.searchParams.get('title')||'',body:body, + label:u.searchParams.get('labels')||'',key:m?m[1]:'', + row:a.closest('li')||a.closest('details.qsec')};} +function gh(path,opts,tok){opts=opts||{};opts.headers={ + 'Authorization':'Bearer '+(tok||token()),'Accept':'application/vnd.github+json', + 'Content-Type':'application/json','X-GitHub-Api-Version':'2022-11-28'}; + return fetch(API+'/repos/'+repo()+path,opts);} +async function createIssue(req){ + var r=await gh('/issues',{method:'POST',body:JSON.stringify( + {title:req.title,body:req.body,labels:req.label?[req.label]:[]})}); + if(!r.ok){throw new Error('GitHub said '+r.status+ + (r.status===401||r.status===403||r.status===404? + ' \u2014 check the token: Issues read & write on '+repo():''));} + return (await r.json()).number;} +function hideRow(req){if(req.row)req.row.hidden=true; + if(req.key){var h=hiddenMap();h[req.key]=Date.now();saveHidden(h);}} +function restoreHidden(){var h=hiddenMap(),seen={},now=Date.now(),changed=false; + var as=document.querySelectorAll("a.act[href*='/issues/new']"); + for(var i=0;i
"+ + "
"; + (a.closest('li')||a.parentNode).appendChild(f); + var ta=f.querySelector('textarea');ta.focus(); + f.querySelector('.no').onclick=function(){f.remove();}; + f.querySelector('.go').onclick=function(){var n=ta.value.trim(); + if(n){req.body=req.body.replace(/^notes: \(optional[^\n]*$/m, + 'notes: '+n.replace(/\s*\n\s*/g,' '));} + f.remove();run(a,req);};} +document.addEventListener('click',function(e){ + var a=e.target.closest("a.act[href*='/issues/new']"); + if(!a||!token()||!repo())return; + var req=request(a);if(!req)return; + e.preventDefault(); + if(req.label==='queue-intake'||req.label==='queue-cite')notesForm(a,req); + else run(a,req);}); +function keyChip(){var c=document.getElementById('onetap');if(!c)return; + var on=!!token();c.textContent=on?'\ud83d\udd11 one-tap on \u00b7 sign out': + '\ud83d\udd11 set up one-tap';c.className=on?'on':''; + c.onclick=async function(){ + if(token()){try{localStorage.removeItem(TOK);}catch(e){}keyChip(); + toast('one-tap off \u2014 the buttons open GitHub again');return;} + var t=prompt('One-tap mode: paste a fine-grained GitHub token with Issues: '+ + 'read & write on '+repo()+'. It is stored only in this browser and used '+ + 'only to open the queue issues from here.'); + if(!t)return;t=t.trim(); + try{var r=await gh('',{},t);if(!r.ok)throw new Error('GitHub said '+r.status); + var j=await r.json(); + if(!(j.permissions&&j.permissions.push))throw new Error( + 'that token cannot write to '+repo()); + localStorage.setItem(TOK,t);keyChip(); + toast('one-tap on \u2014 every button now files from here');} + catch(e){toast(e.message,true);}};} +restoreHidden();keyChip(); """ @@ -1255,16 +1366,26 @@ def _fresh_span(fresh: dict, now_text: str, stale_text: str, repo_url = _repo_url(snapshot) github_link = (f' ยท ' "GitHub Page" if repo_url else "") + # One-tap mode needs the API's owner/repo and a place to turn it on; both + # drop out with the repo identity, like every other GitHub-facing chip. + owner, repo = snapshot.get("owner"), snapshot.get("repo") + body_attrs = (f' data-repo="{_html.escape(owner + "/" + repo, quote=True)}"' + if repo_url else "") + onetap = (" ยท " if repo_url else "") return f""" PyAutoMemory Dashboard - + {hero} {stats} -

markdown version{github_link}

+

markdown version{github_link}{onetap}

+
{filings_block}

arXiv inbox (suggested overnight โ€” un-acted papers lapse after {inbox_actions.INBOX_WINDOW_DAYS} days)

diff --git a/tests/test_board.py b/tests/test_board.py index 8790265..41e86e9 100644 --- a/tests/test_board.py +++ b/tests/test_board.py @@ -169,8 +169,11 @@ def test_paper_links_and_issue_actions(tmp_path): def test_no_issue_links_without_a_remote(tmp_path): """A spawned-template checkout (no remote) renders without dead buttons.""" html = board.render(board.collect(_tree(tmp_path)), "html") - assert "issues/new" not in html - assert "arxiv.org/search" in html # paper links still work + # The one-tap script names the link shape it intercepts; that is not a + # button, so judge the markup with the script stripped out. + markup = re.sub(r"", "", html, flags=re.DOTALL) + assert "issues/new" not in markup + assert "arxiv.org/search" in markup # paper links still work def test_md_lists_papers(tmp_path): @@ -216,8 +219,13 @@ def test_html_is_self_contained(tmp_path): # no repo identity in this snapshot โ†’ the GitHub Page segment drops out assert "GitHub Page" not in out assert "src=" not in out and "(.*)", out, re.DOTALL).group(1) + assert out.count("fetch(") == script.count("fetch(") == 1 + assert "fetch(API+" in script and "API='https://api.github.com'" in script + stripped = re.sub(r'data-cmd="[^"]*"', "", out.replace(script, "")) for m in re.finditer(r"(?:http|https)://", stripped): before = stripped[max(0, m.start() - 30):m.start()] assert 'href="' in before or "href='" in before, f"non-href URL at {m.start()}" @@ -718,3 +726,32 @@ def test_pending_filings_without_a_remote_still_name_the_branch(tmp_path): for text in (board._render_md(snap), board._render_html(snap)): assert "queue-filing/issue-7" in text assert "compare/main" not in text + + +# --- one-tap mode --------------------------------------------------------------- +def test_one_tap_mode_is_wired_only_with_a_remote(tmp_path): + """The chip, the API's owner/repo and the toast slot all hang off the repo + identity; a spawned template with no remote renders none of them.""" + snap = _snap_with_remote(tmp_path) + html = board._render_html(snap) + assert 'data-repo="PyAutoLabs/PyAutoMemory"' in html + assert "id='onetap'" in html and 'id="toast"' in html + snap["owner"] = snap["repo"] = "" + bare = board._render_html(snap) + assert "data-repo=" not in bare and "id='onetap'" not in bare + + +def test_one_tap_reads_the_request_back_out_of_the_link(tmp_path): + """No second copy of the issue in the markup: the script parses title, + body and label from the prefilled link every button already carries, so + the workflows see exactly the issue the link would have opened.""" + html = board._render_html(_snap_with_remote(tmp_path)) + assert "searchParams.get('body')" in html + assert "searchParams.get('labels')" in html + assert "/^(?:line|date): (.*)$/m" in html # the row's hide key + assert "restoreHidden();keyChip();" in html + + +def test_one_tap_never_touches_the_page_without_a_token(tmp_path): + html = board._render_html(_snap_with_remote(tmp_path)) + assert "if(!a||!token()||!repo())return;" in html