35c30ecce0
F-CORPUS: ключевые документы приёмки liderra.ru лежали untracked — мастер- хэндофф ссылался на отсутствующие в git файлы (битые ссылки в новом клоне). Закоммичены: R0–R5 + stepbystep ранбуки, хартия, prod-logic-map, эфир-хэндофф, imitation-checks-table, live-demo/ (эфир-плеер) + смежные specs/планы серий f1-card/phase1/televizor/g1/g2 (решение владельца — «корпус + смежные»). F-DELPROJ: пункт №15 checks-table → «удаление проекта со сделками запрещено (422), сделки целы» (было неточно «сделки сохранены», сверено по ProjectService::delete). Реестр находок: статусы F-DEPTRAC/F-CSV/F-REMIND/F-DELPROJ/F-CORPUS → закрыто. .gitleaks.toml: ранбуки приёмки добавлены в allowlist (синтетические тест- телефоны, та же категория что plans/specs/audits). live-demo HTML: stylelint --fix (#fff→#ffffff). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
115 lines
8.2 KiB
HTML
115 lines
8.2 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="ru">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>ЖИВОЙ ЭФИР · приёмка liderra.ru</title>
|
|
<style>
|
|
:root{--teal:#0F6E56;--ivory:#F6F3EC;--noir:#012019;--amber:#caa23a;--ok:#1a9e74;--red:#e5484d;--ink:#15302a;--mut:#5d7771;}
|
|
*{box-sizing:border-box;} html,body{margin:0;}
|
|
body{background:var(--noir);color:var(--ivory);font-family:Inter,'Segoe UI',Arial,sans-serif;font-size:13.5px;}
|
|
header{background:var(--teal);padding:11px 18px;display:flex;align-items:center;gap:12px;position:sticky;top:0;z-index:50;}
|
|
header .dot{width:11px;height:11px;border-radius:50%;background:#e5484d;animation:pulse 1.4s infinite;}
|
|
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(229,72,77,.7)}70%{box-shadow:0 0 0 9px rgba(229,72,77,0)}100%{box-shadow:0 0 0 0 rgba(229,72,77,0)}}
|
|
header h1{font-size:15px;margin:0;font-weight:600;}
|
|
header .stat{margin-left:auto;font-size:12.5px;font-weight:700;background:#0a4f3e;padding:4px 12px;border-radius:13px;}
|
|
header .ctl{font-size:12.5px;font-weight:800;padding:4px 12px;border-radius:13px;}
|
|
header .ctl.play{background:#0a4f1f;color:#7dffb4;border:1px solid #1c7e44;}
|
|
header .ctl.pause{background:#5e1414;color:#ff9a9a;border:1px solid #8a2b2b;}
|
|
header .cbtn{border:0;border-radius:8px;padding:6px 13px;font-size:13px;font-weight:800;cursor:pointer;}
|
|
header .cbtn.play{background:#7ee0bd;color:#0d2a22;} header .cbtn.play:hover{background:#9af0d0;}
|
|
header .cbtn.pause{background:#ffce85;color:#3a2a00;} header .cbtn.pause:hover{background:#ffd99e;}
|
|
.hint{background:#0d6b4f;padding:8px 18px;font-size:12.5px;border-bottom:1px solid #19a079;color:#defff2;}
|
|
.hint b{color:#ffffff;}
|
|
.feed{padding:16px 18px;max-width:1180px;}
|
|
.pgroup{font-size:12px;font-weight:800;text-transform:uppercase;letter-spacing:.5px;color:#7ee0bd;margin:14px 2px 8px;border-bottom:1px dashed #1d5a49;padding-bottom:5px;}
|
|
.step{background:#06281f;border:1px solid var(--teal);border-radius:11px;margin-bottom:16px;overflow:hidden;animation:fly .5s ease;}
|
|
@keyframes fly{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
|
.step.fail{border-color:#b3322f;}
|
|
.step.note{border-color:#3d5a86;}
|
|
.step>h2{margin:0;background:var(--teal);font-size:14px;padding:10px 14px;font-weight:700;display:flex;gap:10px;align-items:center;}
|
|
.step.fail>h2{background:#b3322f;}
|
|
.step.note>h2{background:#274a86;}
|
|
.step>h2 .v{margin-left:auto;font-weight:800;padding:2px 10px;border-radius:11px;font-size:12px;}
|
|
.step>h2 .v.ok{background:#0a4f1f;color:#7dffb4;border:1px solid #1c7e44;}
|
|
.step>h2 .v.fail{background:#3a0c0c;color:#ff9a9a;border:1px solid #8a2b2b;}
|
|
.step>h2 .v.note{background:#0d2440;color:#a9c6ee;border:1px solid #3d5a86;}
|
|
.did{padding:10px 14px;font-size:13px;color:#d8f0e6;border-bottom:1px solid #0e3a2e;}
|
|
.did b{color:#7ee0bd;}
|
|
table.rep{width:100%;border-collapse:collapse;font-size:13px;}
|
|
table.rep th{background:#08312a;color:#9fe3c8;text-align:left;padding:8px 12px;font-size:11px;text-transform:uppercase;letter-spacing:.4px;border-bottom:1px solid #14463a;}
|
|
table.rep td{padding:10px 12px;border-bottom:1px solid #0e3a2e;vertical-align:top;line-height:1.5;}
|
|
table.rep .was{color:#c0b48a;} table.rep .exp{color:#bfe6d6;} table.rep .got{color:#eafff7;font-weight:600;}
|
|
.pill{display:inline-block;font-weight:800;font-size:11px;padding:2px 9px;border-radius:10px;white-space:nowrap;}
|
|
.pill.shot{background:#143a2e;color:#7ee0bd;border:1px solid #1c7e44;}
|
|
.pill.text{background:#2c2a1a;color:#ffd766;border:1px solid #5a4f1a;}
|
|
.inside{margin:12px 14px;background:#2a2410;border:1px solid #5a4f1a;border-radius:9px;padding:11px 13px;font-size:13px;color:#ffe9b8;line-height:1.55;}
|
|
.inside .h{font-weight:800;color:#ffd766;margin-bottom:4px;}
|
|
.shots{display:flex;gap:14px;padding:0 14px 14px;flex-wrap:wrap;align-items:stretch;}
|
|
.shotcard{flex:1 1 44%;min-width:300px;background:#0a3328;border:1px solid #15463a;border-radius:9px;overflow:hidden;display:flex;flex-direction:column;}
|
|
.shotcard .cap{padding:8px 11px;font-size:12.5px;font-weight:700;background:#08312a;border-bottom:1px solid #14463a;color:#bfe6d6;}
|
|
.shotcard img{width:100%;display:block;background:#ffffff;}
|
|
.frame{padding:8px;}
|
|
.wait{padding:40px;text-align:center;color:#6f968a;font-size:14px;}
|
|
@media(max-width:820px){.shotcard{flex:1 1 100%}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<header>
|
|
<span class="dot"></span>
|
|
<h1>ЖИВОЙ ЭФИР · приёмка боевого liderra.ru — отчёт по шагам</h1>
|
|
<span class="stat" id="stat">шагов: 0</span>
|
|
<span class="ctl pause" id="ctl">⏸ на паузе</span>
|
|
<button class="cbtn play" id="bplay" onclick="setCtl('play')">▶ Дальше</button>
|
|
<button class="cbtn pause" id="bpause" onclick="setCtl('pause')">⏸ Пауза</button>
|
|
</header>
|
|
<div class="hint">Каждый шаг прилетает сюда <b>по ходу</b>, как только я его сделал. 📸 — что видно на экране (скриншот) · 💡 — что внутри портала (простыми словами) · зелёный/красный итог — <b>сошлось / не сошлось</b>. Страница обновляется сама.</div>
|
|
<div class="feed" id="feed"><div class="wait" id="wait">⏳ жду первый шаг…</div></div>
|
|
|
|
<script>
|
|
const pad=n=>String(n).padStart(4,'0');
|
|
let rendered=0, lastPunkt=null;
|
|
async function ft(u){try{const r=await fetch(u+'?t='+Date.now());return r.ok?await r.text():null;}catch(e){return null;}}
|
|
function esc(s){return (s||'').replace(/&/g,'&').replace(/</g,'<');}
|
|
function card(d){
|
|
const feed=document.getElementById('feed');
|
|
if(d.punkt && d.punkt!==lastPunkt){lastPunkt=d.punkt;const g=document.createElement('div');g.className='pgroup';g.textContent='▸ '+d.punkt;feed.appendChild(g);}
|
|
const isFail=d.status==='fail', isNote=d.status==='note';
|
|
const el=document.createElement('div'); el.className='step'+(isFail?' fail':(isNote?' note':''));
|
|
let rows=(d.rows||[]).map(r=>{
|
|
const pill=r.proof==='shot'?'<span class="pill shot">📸 на экране</span>':'<span class="pill text">💡 словами</span>';
|
|
return `<tr><td>${esc(r.what)}</td><td class="was">${esc(r.was)}</td><td class="exp">${esc(r.exp)}</td><td class="got">${esc(r.got)}</td><td>${pill}</td></tr>`;
|
|
}).join('');
|
|
let shots=(d.shots||[]).map(s=>`<div class="shotcard"><div class="cap">${esc(s.cap)}</div><div class="frame"><img src="${s.img}" alt="shot" loading="lazy"></div></div>`).join('');
|
|
el.innerHTML =
|
|
`<h2>${esc(d.title)}<span class="v ${isFail?'fail':(isNote?'note':'ok')}">${isFail?'🔴 НЕ СОШЛОСЬ':(isNote?'ℹ️ на прод-прогоне':'✅ СОШЛОСЬ')}</span></h2>`+
|
|
(d.did?`<div class="did">🎬 <b>Что я сделал:</b> ${esc(d.did)}</div>`:'')+
|
|
(rows?`<table class="rep"><tr><th>Что смотрим</th><th>Было</th><th>Ожидали</th><th>Стало (на самом деле)</th><th>Чем</th></tr>${rows}</table>`:'')+
|
|
(d.inside?`<div class="inside"><div class="h">💡 Что произошло внутри портала (простыми словами)</div>${esc(d.inside)}</div>`:'')+
|
|
(shots?`<div class="shots">${shots}</div>`:'');
|
|
feed.appendChild(el);
|
|
el.scrollIntoView({behavior:'smooth',block:'end'});
|
|
}
|
|
async function setCtl(v){try{await fetch('control.php?set='+v+'&t='+Date.now(),{cache:'no-store'});}catch(e){}ctlPoll();}
|
|
async function ctlPoll(){
|
|
const s=(await ft('control.php'))||'pause';
|
|
const e=document.getElementById('ctl');
|
|
e.className='ctl '+(s==='play'?'play':'pause');
|
|
e.textContent=s==='play'?'▶ идёт':'⏸ на паузе';
|
|
}
|
|
async function poll(){
|
|
ctlPoll();
|
|
const c=parseInt(await ft('steps/count.txt'))||0;
|
|
document.getElementById('stat').textContent='шагов: '+c;
|
|
while(rendered<c){
|
|
const t=await ft('steps/'+pad(rendered+1)+'.json'); if(t===null) break;
|
|
let d; try{d=JSON.parse(t);}catch(e){break;}
|
|
const w=document.getElementById('wait'); if(w) w.remove();
|
|
card(d); rendered++;
|
|
}
|
|
}
|
|
setInterval(poll,1500); poll();
|
|
</script>
|
|
</body>
|
|
</html>
|