diff --git a/README.md b/README.md index ecc667a..013c69d 100644 --- a/README.md +++ b/README.md @@ -430,6 +430,9 @@ NAVI_SSH_PASS='<密码>' bash deploy/zd-host.sh v50 | `web/patch-index.py` | 幂等地把 `navi.css` / `navi.js` 注入 Dashy 的 index.html | | `web/extension/install.html` | 插件安装引导页(含同步令牌展示) | | `extension/chrome/` `extension/firefox/` | 浏览器扩展(MV3 / MV2) | +| `deploy/verify-matrix.js` | 生产验收矩阵:6 尺寸 × 3 视图 × 深浅色,断言滚动/文字亮度/深色生效/零异常(需 `PORTAL_COOKIE`) | +| `deploy/verify-workspace.js` | 工作空间深色与"无法内嵌"弹窗验收(触发 blocked 态并读 computed style) | +| `deploy/verify-scroll.js` | 多视口纵向滚动复现/回归检查(含矮窗口) | | `deploy/zd-host.sh` | **合并后**的部署脚本(host 模式,接受数秒中断,失败自动回滚) | | `portal/`(index.html、404.html、500.html、portal.css、favicon) | 门户静态页与自定义错误页,进镜像的 `/app/portal` | | `admin-server/auth.go` | 容器内置统一登录(HMAC 会话 cookie、恒定时间比较、跳转白名单) | @@ -795,6 +798,7 @@ MIT 文档中的 `10.0.0.10`/`*.example.com` 示例地址。发布前自检: ```bash -git grep -nE 'NAVI_(SYNC|ADMIN)_TOKEN=.|PORTAL_PASSWORD=.+' -- . || echo '无凭据' +# 只匹配"看起来是真值"的赋值:排除 <占位>、${VAR:-}、change-me 之类 +git grep -nE '(NAVI_(SYNC|ADMIN)_TOKEN|PORTAL_PASSWORD|PORTAL_SESSION_SECRET)=[^<${]' -- . || echo '无凭据' git ls-files | grep -E '(^|/)conf\.yml$|\.env$|portal-auth\.json$|(fullchain|privkey)\.pem$' || echo '无敏感文件' ``` diff --git a/deploy/verify-matrix.js b/deploy/verify-matrix.js new file mode 100644 index 0000000..07be696 --- /dev/null +++ b/deploy/verify-matrix.js @@ -0,0 +1,53 @@ +const { chromium }=require('playwright-core'); +function rgb(s){const m=s.match(/[\d.]+/g);return m?m.slice(0,4).map(Number):[]} +function L(s){const [r=0,g=0,b=0]=rgb(s);return Math.round(.2126*r+.7152*g+.0722*b)} +const C=process.env.PORTAL_COOKIE; +(async()=>{ + const b=await chromium.connectOverCDP('http://127.0.0.1:9222'); + const views=[['/','home'],['/minimal','minimal'],['/workspace','workspace']]; + const sizes=[[1280,720],[1024,600],[900,520],[390,780]]; + const rows=[]; let bad=0; + for(const [w,h] of sizes){ + for(const [path,name] of views){ + const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:w,height:h}}); + await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]); + const p=await ctx.newPage(); const errs=[]; p.on('pageerror',e=>errs.push(String(e))); + await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>localStorage.removeItem('navi-theme')); + await p.goto('https://dashy.example.com'+path,{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3200); + const r=await p.evaluate(()=>{ + const g=s=>{const e=document.querySelector(s);if(!e)return null;const c=getComputedStyle(e);return {c:c.color,bg:c.backgroundColor}}; + const de=document.documentElement; + return {vScroll:de.scrollHeight>innerHeight+2,overflow:de.scrollHeight-innerHeight, + hScroll:de.scrollWidth>innerWidth+2, + title:g('header h1')||g('.minimal-home .title-and-search h1'), + sec:g('div.collapsable .section-header h3')||g('.minimal-headings-row .headings'), + item:g('a.item .text'),desc:g('a.item .description'), + side:g('nav.side-bar .side-bar-item'),content:g('.work-space .web-content'), + footer:g('.navi-footer')}; + }); + const t=r.title?L(r.title.c):0, s=r.sec?L(r.sec.c):0, it=r.item?L(r.item.c):0, d=r.desc?L(r.desc.c):0; + const side=r.side?L(r.side.bg):null, content=r.content?L(r.content.bg):null; + const foot=r.footer?L(r.footer.bg):null; + const row={size:`${w}x${h}`,view:name,scroll:r.vScroll?'SCROLL':'-',overflow:r.overflow,hScroll:r.hScroll?'HSCROLL':'-',title:t,sec:s,item:it,desc:d,sideBg:side,contentBg:content,footBg:foot,err:errs.length}; + const fail=[]; + if(r.vScroll && name!=='home')fail.push('滚动+'+r.overflow); // 首页内容长,允许滚动 + if(r.hScroll)fail.push('横向滚动'); + if(!t)fail.push('未取到标题'); + if(t&&t<190)fail.push('深色未生效/标题亮度'+t); + if(it&&it<190)fail.push('卡片字'+it); + if(d&&d<130)fail.push('说明字'+d); + if(side!==null&&side>120)fail.push('侧栏白底'+side); + if(content!==null&&content>120)fail.push('内嵌白底'+content); + if(errs.length)fail.push('异常'+errs.length); + row.result=fail.length?('✗ '+fail.join(' / ')):'✓'; + if(fail.length) bad++; + rows.push(row); + if(name!=='home'){ await p.screenshot({path:`/tmp/m-${name}-${w}x${h}.png`}); } + await ctx.close(); + } + } + for(const r of rows) console.log(`${r.size.padEnd(9)} ${r.view.padEnd(10)} 滚动=${r.scroll.padEnd(7)} 溢=${String(r.overflow).padStart(4)} 标题=${String(r.title).padStart(3)} 卡片=${String(r.item).padStart(3)} 说明=${String(r.desc).padStart(3)} 侧栏底=${String(r.sideBg).padStart(3)} 内嵌底=${String(r.contentBg).padStart(3)} 备案底=${String(r.footBg).padStart(3)} ${r.result}`); + console.log(bad? `FAILED_ROWS=${bad}` : 'ALL_OK'); + await b.close(); + if(bad) process.exitCode=2; +})().catch(e=>{console.error(e);process.exit(1)}); diff --git a/deploy/verify-scroll.js b/deploy/verify-scroll.js new file mode 100644 index 0000000..b6470e9 --- /dev/null +++ b/deploy/verify-scroll.js @@ -0,0 +1,32 @@ +const { chromium }=require('playwright-core'); +const C=process.env.PORTAL_COOKIE; +(async()=>{ + const b=await chromium.connectOverCDP('http://127.0.0.1:9222'); + for(const [w,h] of [[1280,720],[1280,600],[1024,600],[900,520],[768,600],[390,780]]){ + const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:w,height:h}}); + await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]); + const p=await ctx.newPage(); + await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>localStorage.removeItem('navi-theme')); + await p.goto('https://dashy.example.com/minimal',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(1800); + const r=await p.evaluate(()=>{ + const de=document.documentElement; + const foot=document.querySelector('.navi-footer'); + const fr=foot?foot.getBoundingClientRect():null; + // 谁在撑高页面 + let worst=null; + for(const e of document.querySelectorAll('body *')){ + const b=e.getBoundingClientRect(); + if(b.height<4) continue; + if(de.scrollHeight>innerHeight+2 && b.bottom>innerHeight+2){ + if(!worst||b.bottom>worst.bottom) worst={cls:e.className.toString().slice(0,60),bottom:Math.round(b.bottom),h:Math.round(b.height)}; + } + } + return {scrollH:de.scrollHeight,viewH:innerHeight,overflow:de.scrollHeight-innerHeight,scrollable:de.scrollHeight>innerHeight+2, + bodyH:document.body.scrollHeight,footerBottom:fr?Math.round(fr.bottom):null,worst}; + }); + console.log(`${String(w).padStart(4)}x${String(h).padEnd(4)} docH=${String(r.scrollH).padEnd(5)} viewH=${String(r.viewH).padEnd(5)} 溢出=${String(r.overflow).padEnd(4)} 可滚=${r.scrollable?'YES':'no '} footer底=${r.footerBottom} 元凶=${r.worst?JSON.stringify(r.worst):'无'}`); + await p.screenshot({path:`/tmp/mini-${w}x${h}.png`}); + await ctx.close(); + } + await b.close(); +})().catch(e=>{console.error(e);process.exit(1)}); diff --git a/deploy/verify-workspace.js b/deploy/verify-workspace.js new file mode 100644 index 0000000..35a9eab --- /dev/null +++ b/deploy/verify-workspace.js @@ -0,0 +1,35 @@ +const { chromium }=require('playwright-core'); +function rgb(s){const m=s.match(/[\d.]+/g);return m?m.slice(0,4).map(Number):[]} +function L(a){return Math.round(.2126*(a[0]||0)+.7152*(a[1]||0)+.0722*(a[2]||0))} +const C=process.env.PORTAL_COOKIE; +(async()=>{ + const b=await chromium.connectOverCDP('http://127.0.0.1:9222'); + const ctx=await b.newContext({ignoreHTTPSErrors:true,viewport:{width:1400,height:900}}); + await ctx.addCookies([{name:'portal_session',value:C,domain:'.example.com',path:'/',httpOnly:true,secure:true,sameSite:'Lax'}]); + const p=await ctx.newPage(); const errs=[]; p.on('pageerror',e=>errs.push(String(e))); + await p.emulateMedia({colorScheme:'dark'}); await p.addInitScript(()=>{try{localStorage.removeItem('navi-theme')}catch(e){}}); + await p.goto('https://dashy.example.com/workspace',{waitUntil:'networkidle',timeout:90000}); await p.waitForTimeout(3000); + const n=await p.evaluate(()=>[...document.querySelectorAll('nav.side-bar a,nav.side-bar button,.side-bar-item')].length); + let modal=null; + for(let i=0;i{const e=[...document.querySelectorAll('nav.side-bar a,nav.side-bar button,.side-bar-item')][i];e&&e.click()},i); + await p.waitForTimeout(3500); + modal=await p.evaluate(()=>{ + const card=document.querySelector('.navi-ws-modal-card'); if(!card) return null; + const c=getComputedStyle(card); const t=document.querySelector('.navi-ws-modal-title'); + return {bg:c.backgroundColor, title:t?getComputedStyle(t).color:null, text:(card.textContent||'').trim().slice(0,60)}; + }); + if(modal) break; + } + const r=await p.evaluate(()=>{ + const g=s=>{const e=document.querySelector(s);if(!e)return null;const c=getComputedStyle(e);return c.backgroundColor}; + return {ws:(document.querySelector('.work-space')||{dataset:{}}).dataset?.naviWs||null, + modalCard:g('.navi-ws-modal-card'),modalOk:g('.navi-ws-modal-ok'),webContent:g('.work-space .web-content')}; + }); + console.log(JSON.stringify({sidebarItems:n,modalSeen:!!modal,modal,r,errors:errs},null,1)); + await p.screenshot({path:'/tmp/ws-v50-modal.png',fullPage:true}); + await ctx.close(); await b.close(); + const ok = !modal || (L(rgb(modal.bg))<95 && L(rgb(modal.title||'rgb(255,255,255)'))>190); + console.log(ok?'MODAL_DARK_OK':'MODAL_STILL_LIGHT'); + if(!ok) process.exitCode=2; +})().catch(e=>{console.error(e);process.exit(1)});