Add UpToDate monitor-first live stack taste test (#85)
This commit is contained in:
1 parent
090400a95e
commit
fae0f455f4
3 files changed
+48
No files matched your search
@@ -0,0 +1,7 @@
|
||||
# UpToDate — issue #85
|
||||
|
||||
Monitor-first taste test inspired by Chai. Buildless HTML/JavaScript; no dependencies or secrets. Serve `site/` over HTTP and paste a public GitHub repository, or press Try Express. The page scans the default branch at an immutable commit, inventories up to 24 files, checks up to 12 unique dependencies against npm and OSV, and links repository releases. Download JSON evidence to retain a scan.
|
||||
|
||||
Manual refresh or timed refresh while the page remains open. No autonomous edits, unattended monitoring, private repository support, authentication or alerts. Public unlisted URL: https://notplanned.teahub.xyz/v/up-to-date/. GitHub/API limits, CORS or network failures are explicit unknowns. Docker/Actions and unsupported lock formats are inventory-only; exact npm package-lock / Python requirement pins have OSV coverage. No advisory matches is not proof of safety. npm metadata dates are release times; deprecation notices are observed at scan time. Nested duplicate dependency versions remain in inventory, but only one preferred exact version per package is signal-checked. File and dependency caps mean results are partial, not exhaustive.
|
||||
|
||||
Canonical source: matcha/notplanned ventures/up-to-date/. Deploy using existing venture-day static publication infrastructure; do not replace shared services. Signup uses the existing NotPlanned endpoint. Free monitor; hosted/team offerings are ideas only.
|
||||
@@ -0,0 +1,33 @@
|
||||
'use strict';
|
||||
const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
||||
const link=(url,label)=>`<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)}</a>`;
|
||||
let report=null, previous=null, timer=null;
|
||||
async function json(url,options={}){const response=await fetch(url,{...options,signal:AbortSignal.timeout(15000)});if(!response.ok)throw new Error(`HTTP ${response.status}${response.status===403||response.status===429?' (API limit or denied access)':''}`);return response.json();}
|
||||
function parseRepo(value){let s=value.trim().replace(/\/$/,'');if(s.startsWith('https://github.com/'))s=s.slice(19);if(!/^[\w.-]+\/[\w.-]+$/.test(s))throw new Error('Enter owner/repository or an https://github.com/owner/repository URL. Public GitHub only in v0.');return s.replace(/\.git$/,'');}
|
||||
function parseFile(path,text,url,items,gaps){const add=(name,version,ecosystem,exact=false,coverage='Declared; installed version unknown')=>items.push({name,version,ecosystem,exact,path,url,coverage});
|
||||
try{
|
||||
if(path.endsWith('package.json')){const p=JSON.parse(text);for(const kind of ['dependencies','devDependencies','peerDependencies','optionalDependencies'])for(const [n,v]of Object.entries(p[kind]||{}))add(n,v,'npm',false,kind);for(const[n,v]of Object.entries(p.engines||{}))add(n,v,'runtime');}
|
||||
else if(path.endsWith('package-lock.json')){const p=JSON.parse(text);if(p.packages){for(const[n,v]of Object.entries(p.packages)){if(n.startsWith('node_modules/')&&!n.slice(13).includes('/node_modules/')&&v.version)add(n.slice(13),v.version,'npm',true,'Resolved lockfile version; runtime not verified');}}else for(const[n,v]of Object.entries(p.dependencies||{}))if(v.version)add(n,v.version,'npm',true,'Resolved lockfile version');}
|
||||
else if(/requirements[^/]*\.txt$/.test(path)){for(const line of text.split('\n')){const m=line.match(/^([\w.-]+)(?:\[[^\]]+\])?\s*(==|>=|~=|<=|>|<)?\s*([^;#\s]*)/);if(m)add(m[1],m[3]||'unversioned','PyPI',m[2]==='==',m[2]==='=='?'Exact manifest pin':'Range / installed version unknown');}}
|
||||
else if(/Dockerfile[^/]*$/.test(path)){for(const m of text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/gmi))add(m[1].split(':')[0],m[1],'container');}
|
||||
else if(path.includes('.github/workflows/')){for(const m of text.matchAll(/\buses:\s*['"]?([^\s'"#]+)/g))add(m[1].split('@')[0],m[1].split('@')[1]||'unversioned','action');}
|
||||
else {add(path.split('/').pop(),'Detected','stack file');gaps.push(`${path}: detected but full parser unavailable (including yarn/pnpm/Python project lock formats).`);}
|
||||
}catch(e){gaps.push(`${path}: parse failed: ${e.message}`);}}
|
||||
function signal(title,detail,url,date,severity='info'){report.signals.push({title,detail,url,date,severity});}
|
||||
async function scan(){if($('run').disabled)return;let repo;try{repo=parseRepo($('repo').value)}catch(e){$('status').textContent=e.message;return;}$('run').disabled=true;$('demo').disabled=true;$('export').disabled=true;$('results').hidden=true;
|
||||
report={repo,checkedAt:new Date().toISOString(),items:[],signals:[],gaps:[],files:[]};const gaps=report.gaps;try{
|
||||
$('status').textContent='Reading repository metadata…';const api=`https://api.github.com/repos/${repo}`;const meta=await json(api);const branch=meta.default_branch;const commit=await json(`${api}/commits/${encodeURIComponent(branch)}`);report.commit=commit.sha;report.branch=branch;
|
||||
const tree=await json(`${api}/git/trees/${commit.sha}?recursive=1`);if(tree.truncated)gaps.push('GitHub tree is truncated; inventory may omit files.');const paths=tree.tree.filter(f=>f.type==='blob'&&/(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|requirements[^/]*\.txt|pyproject\.toml|poetry\.lock|uv\.lock|go\.mod|Cargo\.toml|Dockerfile[^/]*|\.nvmrc|\.node-version)$|(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(f.path));if(paths.length>24)gaps.push(`${paths.length} relevant files found; first 24 scanned.`);
|
||||
for(const f of paths.slice(0,24)){$('status').textContent=`Inspecting ${f.path}…`;const url=`https://github.com/${repo}/blob/${commit.sha}/${f.path}`;try{if(f.size>400000){gaps.push(`${f.path}: exceeds 400 KB limit.`);continue;}const blob=await json(`${api}/git/blobs/${f.sha}`);const bytes=Uint8Array.from(atob(blob.content.replace(/\s/g,'')),c=>c.charCodeAt(0));const text=new TextDecoder().decode(bytes);report.files.push({path:f.path,url});parseFile(f.path,text,url,report.items,gaps);}catch(e){gaps.push(`${f.path}: ${e.message}`);}}
|
||||
if(!report.items.length)gaps.push('No supported stack declarations found. This is not evidence of a dependency-free repository.');
|
||||
const unique=new Map();for(const i of report.items){const key=i.ecosystem+':'+i.name;const old=unique.get(key);if(!old||i.exact&&!old.exact)unique.set(key,i);}const dependencies=[...unique.values()].filter(i=>['npm','PyPI'].includes(i.ecosystem));if(dependencies.length>12)gaps.push(`${dependencies.length} dependencies inventoried; release/advisory checks limited to first 12 unique dependencies.`);
|
||||
for(const i of dependencies.slice(0,12)){$('status').textContent=`Checking upstream signals for ${i.name}…`;if(i.ecosystem==='npm')try{const p=await json(`https://registry.npmjs.org/${encodeURIComponent(i.name)}`);const latest=p['dist-tags']?.latest, current=i.exact?p.versions?.[i.version]:null;signal(`${i.name} · latest ${latest||'unknown'}`,`Observed ${i.version}. ${i.exact&&latest===i.version?'Matches npm latest.': 'Review compatibility and release notes; a differing version/range is not proof an upgrade is required.'}`,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}`,p.time?.[latest]||report.checkedAt);if(current?.deprecated)signal(`${i.name} · deprecated version`,current.deprecated,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}/v/${encodeURIComponent(i.version)}`,report.checkedAt,'alert');else if(!i.exact)gaps.push(`${i.name}: installed version and version-specific deprecation unknown (range).`);}catch(e){gaps.push(`${i.name}: npm release/deprecation check unavailable: ${e.message}`);}
|
||||
if(i.exact&&/^\d+\.\d+/.test(i.version))try{const result=await json('https://api.osv.dev/v1/query',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({package:{name:i.name,ecosystem:i.ecosystem},version:i.version})});const active=(result.vulns||[]).filter(v=>!v.withdrawn);if(!active.length)signal(`${i.name} ${i.version} · no OSV matches`,`OSV returned no published matches for this exact version at scan time. This is not a security certification.`, 'https://osv.dev/',report.checkedAt);for(const v of active)signal(`${i.name} ${i.version} · ${v.id}`,`${v.summary||'Published vulnerability'} — lockfile/pin matches; runtime reachability and exploitability require review.`,`https://osv.dev/vulnerability/${encodeURIComponent(v.id)}`,v.modified||v.published||report.checkedAt,'alert');}catch(e){gaps.push(`${i.name}: OSV check unavailable: ${e.message}`);}else gaps.push(`${i.name}: vulnerability status unknown; no supported exact version.`);}
|
||||
try{const releases=await json(`${api}/releases?per_page=3`);for(const r of releases)signal(`Repository release · ${r.tag_name}`,`${r.name||r.tag_name}${r.prerelease?' (prerelease)':''}. Review release notes for changes affecting this stack.`,r.html_url,r.published_at);if(!releases.length)gaps.push('Repository has no GitHub releases; tags/changelog not evaluated.');}catch(e){gaps.push(`Repository releases unavailable: ${e.message}`);}
|
||||
gaps.push('Docker and Action refs: inventory only; release, security and deprecation coverage unknown. Private repos, GitLab, runtime discovery and unattended alerts are unsupported.');
|
||||
const signature=JSON.stringify(report.items.map(i=>[i.name,i.version,i.path]));report.comparison=previous?(signature===previous?'No inventory changes since previous scan in this tab.':'Inventory changed since previous scan in this tab; review evidence.'):'First scan in this tab; no previous baseline.';previous=signature;
|
||||
$('status').textContent=`Scan complete · ${report.checkedAt} · ${repo}`;
|
||||
}catch(e){gaps.push(`Scan incomplete: ${e.message}. Check repository visibility, connectivity and GitHub's unauthenticated rate limit (usually 60 requests/hour/IP). Retry later; no clean bill of health is implied.`);$('status').textContent='Scan incomplete — see unknowns below.';}
|
||||
render();$('run').disabled=false;$('demo').disabled=false;$('export').disabled=false;}
|
||||
function render(){$('results').hidden=false;$('summary').innerHTML=`<p><span class="badge">Live API evidence</span> ${report.items.length} observations · ${report.files.length} files · ${esc(report.checkedAt)}</p><p>${esc(report.comparison||'Incomplete scan; no baseline saved.')} ${report.commit?link(`https://github.com/${report.repo}/tree/${report.commit}`,`Source ${report.commit.slice(0,8)}`):''}</p>`;$('inventory').innerHTML=report.items.map(i=>`<tr><td>${esc(i.name)}<br><small>${esc(i.ecosystem)}</small></td><td>${esc(i.version)}</td><td>${link(i.url,i.path)}</td><td>${esc(i.coverage)}</td></tr>`).join('');$('signals').innerHTML=report.signals.map(s=>`<article class="${s.severity==='alert'?'alert':''}"><strong>${esc(s.title)}</strong><p>${esc(s.detail)}</p><small>${link(s.url,'Source')} · source/checked time: ${esc(s.date)}</small></article>`).join('')||'<p>No verified signals collected. See coverage gaps.</p>';$('gaps').innerHTML=report.gaps.map(g=>`<li>${esc(g)}</li>`).join('');}
|
||||
$('scan').onsubmit=e=>{e.preventDefault();scan();};$('demo').onclick=()=>{$('repo').value='https://github.com/expressjs/express';scan();};$('interval').onchange=()=>{clearInterval(timer);if(+$('interval').value)timer=setInterval(scan,+$('interval').value);};$('export').onclick=()=>{const url=URL.createObjectURL(new Blob([JSON.stringify(report,null,2)],{type:'application/json'}));const a=document.createElement('a');a.href=url;a.download='up-to-date-evidence.json';a.click();setTimeout(()=>URL.revokeObjectURL(url),1000);};if(new URLSearchParams(location.search).get('joined')==='1')$('join').innerHTML='<p>Thanks — your interest has been recorded.</p>';
|
||||
@@ -0,0 +1,8 @@
|
||||
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>UpToDate · Stack monitor</title>
|
||||
<style>:root{font-family:Georgia,serif;color:#203b36;background:#f4f2e9}*{box-sizing:border-box}body{margin:0}header,main,footer{max-width:1160px;margin:auto;padding:28px}header{display:flex;justify-content:space-between;border-bottom:1px solid #bbc5b8}a{color:#176e59}h1{font-size:clamp(38px,6vw,70px);font-weight:400;margin:20px 0}h2{font-weight:400}p{line-height:1.6}small,label,button,input,select,.meta,th,td{font-family:Arial,sans-serif}small,.meta{font-size:13px;color:#53645e}.intro{max-width:700px}.scan{display:flex;gap:10px;flex-wrap:wrap;background:#e4e9de;padding:20px;border:1px solid #b8c5b3}input{min-width:180px;flex:1;padding:14px;font-size:16px;border:1px solid #84988d}button,select{padding:14px;border:1px solid #176e59;background:#176e59;color:white;font-size:15px;cursor:pointer}button:disabled{opacity:.5}.secondary{background:transparent;color:#176e59}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;padding:14px 10px;border-bottom:1px solid #cbd2c6;vertical-align:top;overflow-wrap:anywhere}th{font-size:12px;text-transform:uppercase}article{padding:18px;border-left:3px solid #95a68c;background:#fffdf6;margin:12px 0}article.alert{border-color:#b6502d}#status{padding:18px 0;white-space:pre-wrap}#results{overflow-x:auto}.badge{font:12px Arial;padding:4px 8px;background:#dce9dc;display:inline-block}details{margin:20px 0}summary{cursor:pointer}#gaps{line-height:1.7}.bar{display:flex;gap:16px;flex-wrap:wrap;align-items:center}footer{border-top:1px solid #bbc5b8}input:focus,button:focus{outline:3px solid #a0bca4}@media(max-width:600px){header,main,footer{padding:18px}th,td{padding:10px 5px}table{min-width:620px}}
|
||||
</style><header><strong>NotPlanned / UpToDate</strong><small>01 — monitor first</small></header><main><div class="intro"><h1>Your stack moves.<br>Keep an eye on it.</h1><p>Inspect a public GitHub repository. See what it runs, what changed upstream, and which exact versions have published advisories. Read the evidence before deciding what to change.</p></div>
|
||||
<form id="scan" class="scan"><label for="repo" style="width:100%">Public GitHub repository</label><input id="repo" required value="https://github.com/expressjs/express" aria-label="Public GitHub repository"><button id="run">Inspect stack →</button><button type="button" id="demo" class="secondary">Try Express</button></form>
|
||||
<div class="bar"><label>Refresh while open <select id="interval"><option value="0">Manual</option><option value="900000">Every 15 minutes</option><option value="3600000">Every hour</option></select></label><button id="export" class="secondary" disabled>Download evidence</button></div><p class="meta">Live, unauthenticated APIs. No tokens, repository writes, or background service. This unlisted link is publicly accessible, not access-controlled.</p><div id="status" role="status" aria-live="polite">Ready. The Express example is a real repository, not sample data.</div><section id="results" hidden><div id="summary"></div><h2>Stack inventory</h2><table><thead><tr><th>Component</th><th>Observed version</th><th>Evidence</th><th>Coverage</th></tr></thead><tbody id="inventory"></tbody></table><h2>Signals & impact</h2><div id="signals"></div><details open><summary>Unknowns & scan boundaries</summary><ul id="gaps"></ul></details></section>
|
||||
<details><summary>How this taste test works</summary><p>We read up to 24 matching files from the default branch, including nested manifests and CI files; check up to 12 dependencies; and keep source links and timestamps. Exact npm lockfile / Python pins can be checked against OSV. Ranges are declarations, not proof of installed versions. Docker tags and Action refs are inventoried; their vulnerability coverage is unknown. Refresh compares the inventory with your previous scan in this tab. Close the tab and monitoring stops.</p></details>
|
||||
<details id="join"><summary>Follow the experiment</summary><p>Free: this browser monitor. Hosted alerts and team monitoring are early-access ideas, with no checkout or paid service yet.</p><form method="post" action="/api/signup"><input type="hidden" name="venture" value="up-to-date"><input type="hidden" name="plan" value="free"><input name="website" hidden tabindex="-1"><label>Email <input name="email" type="email" required></label><button>Request updates</button></form><p class="meta">Email is stored privately by NotPlanned for experiment updates.</p></details>
|
||||
</main><footer><small>From Chai’s idea · <a href="https://teahub.xyz/matcha/teahub/issues/85">Factory issue #85</a> · No autonomous maintenance in v0.</small></footer><script src="app.js"></script></html>
|
||||
Reference in new issue
Block a user