address review: sanitize tagc, fix customize doc, harden serve-board.ps1

- Sanitize tagc to [A-Za-z0-9_-] before class-attribute interpolation, so a custom
  class name cannot break out of the attribute and inject markup/handlers (template + sample).
- customize.md now points at .tagedit.<name> (the class the renderer actually applies).
- serve-board.ps1: fail when the port is already in use, capture and report the server PID
  (with a stop command), and verify the process did not exit before opening the URL.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 07e720ee-ca02-419e-9adb-300738b6fc76
This commit is contained in:
jennyf19
2026-07-27 15:24:29 -07:00
parent 62bfb28f65
commit 29375678cf
4 changed files with 16 additions and 5 deletions
@@ -331,7 +331,7 @@ function render(){
+`<select class="quadsel" data-quad="${t.id}"><option value=""${q===""?" selected":""}>priority…</option>`
+`<option value="iu"${q==="iu"?" selected":""}>🔴 Do first</option><option value="ins"${q==="ins"?" selected":""}>🔵 Schedule</option>`
+`<option value="niu"${q==="niu"?" selected":""}>🟡 Delegate</option><option value="ninu"${q==="ninu"?" selected":""}>⚪ Later</option></select>`
+`<input class="tagedit ${t.tagc||""}" data-tagedit="${t.id}" value="${escAttr(tag)}" placeholder="label"/></div>`;
+`<input class="tagedit ${safeCls(t.tagc)}" data-tagedit="${t.id}" value="${escAttr(tag)}" placeholder="label"/></div>`;
if(isCounter(t)){
const v=state.counters[t.id],g=t.goal>0?t.goal:1,pct=Math.min(100,Math.round(v/g*100)),inc=t.inc||Math.max(1,Math.round(g/10)),unit=t.unit||"";
c.innerHTML=`<div class="top"><div class="emoji">${esc(t.emoji||"🎯")}</div>
@@ -418,6 +418,7 @@ function tagOf(t){return state.tag[t.id]!==undefined?state.tag[t.id]:(t.tag||"")
function setQuad(id,v){state.quad[id]=v;save();render();}
function setTag(id,v){state.tag[id]=v;save();render();}
function escAttr(s){return esc(s).replace(/"/g,"&quot;");}
function safeCls(s){return (s||"").replace(/[^a-zA-Z0-9_-]/g,"");}
function reorder(from,targetId){if(!from||from===targetId)return;let ids=renderedIds.slice();const fromI=ids.indexOf(from),tgtI=ids.indexOf(targetId);if(fromI<0||tgtI<0)return;ids.splice(fromI,1);let ins=ids.indexOf(targetId);if(fromI<tgtI)ins+=1;ids.splice(ins,0,from);state.order=ids;save();render();}
function sortByPriority(){const rank={iu:0,ins:1,niu:2,ninu:3,"":4};const ids=[...tasks].sort((a,b)=>{const ca=carriedOf(a)?1:0,cb=carriedOf(b)?1:0;if(ca!==cb)return ca-cb;const ra=(rank[quadOf(a)]===undefined?4:rank[quadOf(a)]),rb=(rank[quadOf(b)]===undefined?4:rank[quadOf(b)]);if(ra!==rb)return ra-rb;return orderIndex(a.id)-orderIndex(b.id);}).map(t=>t.id);state.order=ids;save();render();}
function updateOverload(){const el=document.getElementById("overload");if(!el)return;const live=tasks.filter(t=>!carriedOf(t)&&statusOf(t)!=="done").length;if(live>9){el.style.display="block";el.textContent=`That's ${live} active tasks for today — more than a focus board loves. Consider carrying a few to tomorrow (⤳ not today), or use Focus mode to take one at a time. No pressure — this is a nudge, not a rule.`;}else{el.style.display="none";}}
@@ -341,7 +341,7 @@ function render(){
+`<select class="quadsel" data-quad="${t.id}"><option value=""${q===""?" selected":""}>priority…</option>`
+`<option value="iu"${q==="iu"?" selected":""}>🔴 Do first</option><option value="ins"${q==="ins"?" selected":""}>🔵 Schedule</option>`
+`<option value="niu"${q==="niu"?" selected":""}>🟡 Delegate</option><option value="ninu"${q==="ninu"?" selected":""}>⚪ Later</option></select>`
+`<input class="tagedit ${t.tagc||""}" data-tagedit="${t.id}" value="${escAttr(tag)}" placeholder="label"/></div>`;
+`<input class="tagedit ${safeCls(t.tagc)}" data-tagedit="${t.id}" value="${escAttr(tag)}" placeholder="label"/></div>`;
if(isCounter(t)){
const v=state.counters[t.id],g=t.goal>0?t.goal:1,pct=Math.min(100,Math.round(v/g*100)),inc=t.inc||Math.max(1,Math.round(g/10)),unit=t.unit||"";
c.innerHTML=`<div class="top"><div class="emoji">${esc(t.emoji||"🎯")}</div>
@@ -428,6 +428,7 @@ function tagOf(t){return state.tag[t.id]!==undefined?state.tag[t.id]:(t.tag||"")
function setQuad(id,v){state.quad[id]=v;save();render();}
function setTag(id,v){state.tag[id]=v;save();render();}
function escAttr(s){return esc(s).replace(/"/g,"&quot;");}
function safeCls(s){return (s||"").replace(/[^a-zA-Z0-9_-]/g,"");}
function reorder(from,targetId){if(!from||from===targetId)return;let ids=renderedIds.slice();const fromI=ids.indexOf(from),tgtI=ids.indexOf(targetId);if(fromI<0||tgtI<0)return;ids.splice(fromI,1);let ins=ids.indexOf(targetId);if(fromI<tgtI)ins+=1;ids.splice(ins,0,from);state.order=ids;save();render();}
function sortByPriority(){const rank={iu:0,ins:1,niu:2,ninu:3,"":4};const ids=[...tasks].sort((a,b)=>{const ca=carriedOf(a)?1:0,cb=carriedOf(b)?1:0;if(ca!==cb)return ca-cb;const ra=(rank[quadOf(a)]===undefined?4:rank[quadOf(a)]),rb=(rank[quadOf(b)]===undefined?4:rank[quadOf(b)]);if(ra!==rb)return ra-rb;return orderIndex(a.id)-orderIndex(b.id);}).map(t=>t.id);state.order=ids;save();render();}
function updateOverload(){const el=document.getElementById("overload");if(!el)return;const live=tasks.filter(t=>!carriedOf(t)&&statusOf(t)!=="done").length;if(live>9){el.style.display="block";el.textContent=`That's ${live} active tasks for today — more than a focus board loves. Consider carrying a few to tomorrow (⤳ not today), or use Focus mode to take one at a time. No pressure — this is a nudge, not a rule.`;}else{el.style.display="none";}}
@@ -3,8 +3,8 @@
## Theming
Colors live in the `:root` CSS block of `assets/board.template.html`
(`--ember`, `--good`, `--urgent`, backgrounds). The look is warm/dark by default. Tag colors:
`new` (green), `deadline` (red), `career` (purple) — add your own by copying a `.tag.<name>`
rule and passing that name as a task's `tagc`.
`new` (green), `deadline` (red), `career` (purple) — add your own by copying a `.tagedit.<name>`
rule and passing that name as a task's `tagc` (use a plain class name: letters, digits, `-`, `_`).
## v2 — file-backed state (closes the agent loop)
v1 stores progress in the browser (`localStorage`), which the agent can't read back. To let
@@ -7,8 +7,17 @@ param(
)
$full = Join-Path $Dir $File
if (-not (Test-Path $full)) { Write-Error "Board not found: $full"; exit 1 }
Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden
try {
if (Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction SilentlyContinue) {
Write-Error "Port $Port is already in use — choose another with -Port or stop the existing server."; exit 1
}
} catch { } # Get-NetTCPConnection may be unavailable on some platforms; skip the pre-check there.
$proc = Start-Process -FilePath "python" -ArgumentList @("-m","http.server","$Port","--bind","127.0.0.1","--directory","`"$Dir`"") -WindowStyle Hidden -PassThru
Start-Sleep -Seconds 2
if ($proc.HasExited) {
Write-Error "The server exited on startup (code $($proc.ExitCode)) — is Python installed and the port free?"; exit 1
}
$url = "http://localhost:$Port/$File"
Write-Host "Focus board: $url"
Write-Host "Server PID $($proc.Id) — stop it with: Stop-Process -Id $($proc.Id)"
Start-Process $url # opens in default browser; in a Copilot-app session, open a browser canvas to this URL instead