Files
portal/live-demo/efir.html
T
Дмитрий 35c30ecce0 docs(приёмка): корпус приёмочного теста + поправка №15 + статусы реестра
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>
2026-06-21 04:48:50 +03:00

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,'&amp;').replace(/</g,'&lt;');}
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>