| Server IP : 188.114.96.3 / Your IP : 216.73.216.143 Web Server : Apache System : Linux sv058.home.net.pl 6.8.2 husak_dirty_pages_fix #1 SMP PREEMPT_DYNAMIC Tue Apr 2 22:06:51 CEST 2024 x86_64 User : ( 3194769) PHP Version : 8.5.12 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : OFF Directory : /polana/admin/ |
Upload File : |
<?php
declare(strict_types=1);
require_once dirname(__DIR__).'/includes/functions.php';
require_admin_role();
require_once dirname(__DIR__).'/includes/polana_v16_core.php';
$error='';$menus=[];$counts=[];$rows=[];
$area=(string)($_GET['area']??'menu');if(!in_array($area,['menu','blog','pages'],true))$area='menu';
$lang=v16_lang((string)($_GET['lang']??'all'));
$page=max(1,(int)($_GET['page']??1));
$selected=(int)($_GET['menu_id']??0);
try{
v16_init();$menus=v16_menu_ids();$selected=v16_menu_id($selected);
foreach(['menu','blog','pages'] as $a)$counts[$a]=v16_status($a);
$rows=v16_jobs($area,$lang,$selected,$page,35);
}catch(Throwable $e){$error=$e->getMessage();}
$labels=['menu'=>'Menu restauracji','blog'=>'Artykuły bloga','pages'=>'Podstrony WWW'];
$pagePaths=function_exists('p12_paths')?p12_paths():[];
$sampleBlog='';
try{
$slug=(string)db()->query("SELECT slug FROM blog_posts WHERE status='published' AND publish_at<=NOW() ORDER BY id DESC LIMIT 1")->fetchColumn();
if(preg_match('/^[a-z0-9-]+$/',$slug))$sampleBlog='/blog/'.$slug;
}catch(Throwable $e){/* Brak publicznego wzorca artykułu nie blokuje innych stron. */}
$baseQuery=['area'=>$area,'lang'=>$lang,'menu_id'=>$selected];
render_admin_header('Polana — jedno centrum tłumaczeń');
?>
<style>
.v16{--line:#dfe4dc;--forest:#24462d;--muted:#64726b;max-width:1320px;margin:18px auto;padding:14px;font-family:inherit;color:inherit}
.v16 *{box-sizing:border-box}.v16 h1{font-size:clamp(26px,3vw,35px);margin:0 0 6px}.v16 p{line-height:1.55}
.v16 .sub{color:var(--muted);font-size:14px}.v16 .cards{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:20px 0}
.v16 .card{background:#fff;color:#253327;padding:16px;border:1px solid var(--line);border-radius:12px}
.v16 .card strong{font-size:16px}.v16 .counts{display:flex;flex-wrap:wrap;gap:7px;margin-top:10px}.v16 .count{background:#eff3eb;padding:5px 8px;border-radius:7px;font-size:12px}
.v16 .tabs{display:flex;flex-wrap:wrap;gap:8px;margin:16px 0}.v16 .tabs a{border:1px solid #cbd7c7;border-radius:10px;padding:10px 15px;text-decoration:none;color:var(--forest);background:#fff}
.v16 .tabs a.active{background:var(--forest);color:white}.v16 .panel{border:1px solid var(--line);border-radius:13px;padding:19px;margin:15px 0;background:#fff;color:#253327}
.v16 button,.v16 select{font:inherit}.v16 button{border:0;background:var(--forest);color:#fff;border-radius:9px;padding:10px 15px;cursor:pointer}
.v16 button.secondary{border:1px solid var(--line);background:#f1f5ee;color:#243829}.v16 button.warn{background:#965b20}.v16 button:disabled{opacity:.45;cursor:not-allowed}
.v16 .flex{display:flex;align-items:center;gap:10px;flex-wrap:wrap}.v16 select{padding:9px;border:1px solid #ccd5c8;background:white;border-radius:8px}
.v16 textarea{display:block;width:100%;min-height:87px;padding:11px;border:1px solid #cbd6ca;border-radius:9px;font:inherit;resize:vertical;background:#fff;color:#1b281c}
.v16 textarea.long{min-height:150px}.v16 .job{border-top:1px solid var(--line);padding:15px 0}.v16 .job:first-of-type{border-top:0}
.v16 .original{padding:11px;background:#f4f6f2;white-space:pre-wrap;overflow-wrap:anywhere;max-height:260px;overflow:auto;font-size:13px}
.v16 .bulk{position:sticky;top:0;z-index:8;background:#fff;box-shadow:0 2px 8px #0001;border:1px solid var(--line);border-radius:10px;padding:10px;margin:12px 0}
.v16 .status{padding:13px;background:#f0f4eb;border-radius:10px;min-height:47px;white-space:pre-line}.v16 .warnText{color:#985c20}
.v16 .tiny{font-size:12px;color:#657265}.v16 progress{width:100%;height:13px;margin-top:8px}
@media(max-width:720px){.v16 .cards{grid-template-columns:1fr}.v16 .panel{padding:13px}.v16 .flex > button{flex:1 1 45%}.v16 .bulk{top:0}}
</style>
<div class="v16">
<h1>Centrum tłumaczeń</h1>
<p class="sub">Jeden panel dla całej Polany. Polski jest źródłem, Synology tłumaczy EN / DE / ES, a zatwierdzone wersje zapisujemy do istniejącej MariaDB.</p>
<?php if($error):?><div class="panel warnText"><strong>Wymaga sprawdzenia:</strong> <?=e($error)?><p>Wymagana działająca instalacja V9, V12, V13, V14 oraz V15. Żaden istniejący edytor nie jest nadpisywany.</p></div><?php endif;?>
<nav class="tabs" aria-label="Moduły tłumaczeń">
<?php foreach($labels as $code=>$label):?><a class="<?=$area===$code?'active':''?>" href="?<?=e(http_build_query(['area'=>$code,'lang'=>$lang]))?>"><?=e($label)?></a><?php endforeach; ?>
</nav>
<section class="panel" id="v163-actions" style="border:2px solid #aec8a5;background:#f8fbf6;margin:15px 0">
<h2 style="margin:0 0 8px">Szybkie działania — <?=e($labels[$area])?></h2>
<p class="sub" style="margin:0 0 12px">Najpierw sprawdź przykłady. Potem możesz zatwierdzić wszystkie gotowe tłumaczenia w wybranym module i języku, bez przechodzenia przez strony wyników. Nazwy własne i nieaktualne treści zostaną pominięte.</p>
<div class="flex">
<button type="button" class="secondary" id="v163-preview">1. Sprawdź gotowe tłumaczenia</button>
<button type="button" id="v163-all-approve" disabled>2. Zatwierdź wszystkie gotowe</button>
<a href="/admin/polana-centrum-tresci.php" class="sub">← Centrum treści</a>
</div>
<div id="v163-preview-results" class="status" style="margin-top:11px;display:none" aria-live="polite"></div>
<div id="v163-bulk-progress" class="sub" aria-live="polite" style="margin-top:7px"></div>
</section>
<div class="cards">
<?php foreach($labels as $code=>$label):$s=$counts[$code]??[];?><div class="card"><strong><?=e($label)?></strong><div class="counts">
<span class="count">Oczekuje: <b data-counter="<?=$code?>-pending"><?=(int)($s['pending']??0)?></b></span>
<span class="count">Do sprawdzenia: <b data-counter="<?=$code?>-review"><?=(int)($s['review']??0)?></b></span>
<span class="count">Zatwierdzone: <b data-counter="<?=$code?>-approved"><?=(int)($s['approved']??0)?></b></span>
<span class="count">Błędy: <b data-counter="<?=$code?>-errors"><?=(int)($s['errors']??0)?></b></span>
</div></div><?php endforeach;?></div>
<section class="panel">
<h2 style="margin-top:0">Tłumacz automatycznie</h2>
<p class="sub">Pozostaw kartę otwartą. Krótkie żądania PHP tłumaczą kolejne frazy. Błędne zadania nie blokują pozostałych; szczegóły są dostępne w diagnostyce. Możesz zatrzymać oraz później wznowić.</p>
<div class="flex">
<button type="button" id="v16-all">▶ Tłumacz brakujące — wszystko</button>
<button type="button" id="v16-stop" class="warn" disabled>■ Zatrzymaj</button>
</div>
<div id="v16-status" class="status" role="status" aria-live="polite" style="margin-top:13px">Gotowe do uruchomienia. Dla menu: już zapisane przekłady pozostaną bez zmian, jeśli polski oryginał się nie zmienił.</div>
<progress id="v16-progress" style="display:none" max="100" value="0"></progress>
<p class="tiny">Błędy API nie kasują tłumaczeń. Nowe tłumaczenia pojawią się poniżej po odświeżeniu. Do publikacji wymagana jest akceptacja.</p>
<details class="v163-advanced" style="margin-top:18px">
<summary style="cursor:pointer;font-weight:600">Opcje zaawansowane</summary>
<div class="flex" style="margin-top:13px">
<button type="button" id="v16-run" class="secondary">Tłumacz tylko <?=e($area==='menu'?'menu':($area==='blog'?'bloga':'podstrony'))?></button>
<button type="button" id="v16-check-errors" class="secondary">Pokaż przyczyny błędów</button>
<button type="button" id="v16-retry-errors" class="secondary">Ponów nieudane zadania</button>
</div>
<div id="v16-errors" style="margin-top:12px"></div>
</details>
</section>
<?php if($area==='menu' && $menus):?>
<section class="panel">
<h2 style="margin-top:0">Menu — aktualne karty</h2>
<form method="get" class="flex" id="v16-menu-select">
<input type="hidden" name="area" value="menu"><input type="hidden" name="lang" value="<?=e($lang)?>">
<label>Karta <select name="menu_id" onchange="this.form.submit()">
<?php foreach($menus as $id=>$m):?><option value="<?=$id?>" <?=$id===$selected?'selected':''?>><?=e(strtoupper($m['menu_type']).' · '.$m['name'].' #'.$id)?></option><?php endforeach;?></select></label>
</form>
<details style="margin-top:14px"><summary style="cursor:pointer">Przetłumacz ponownie kartę, która ma zły język</summary>
<p class="sub">Stare publiczne wersje pozostaną do czasu zatwierdzenia nowych. Użyj tylko dla źle przetłumaczonej karty.</p>
<div class="flex" id="v16-force-languages"><label><input type="checkbox" value="en"> EN</label><label><input type="checkbox" value="de"> DE</label><label><input type="checkbox" value="es"> ES</label>
<button type="button" class="warn" id="v16-force">Ponów zaznaczone języki tej karty</button>
</div>
</details>
</section>
<?php endif;?>
<section class="panel">
<h2 style="margin-top:0">Sprawdź, popraw i zatwierdź</h2>
<p class="sub">Zaznacz wybrane tłumaczenia, popraw tekst w polu i zatwierdź wszystkie jednym kliknięciem. Lista pokazuje tylko gotowe pozycje aktualnego modułu.</p>
<form method="get" class="flex" style="margin:12px 0">
<input type="hidden" name="area" value="<?=e($area)?>"><input type="hidden" name="menu_id" value="<?=$selected?>">
<label>Język <select name="lang"><?php foreach(['all'=>'Wszystkie','en'=>'EN','de'=>'DE','es'=>'ES'] as $code=>$label):?><option value="<?=$code?>" <?=$code===$lang?'selected':''?>><?=$label?></option><?php endforeach;?></select></label>
<button type="submit" class="secondary">Filtruj</button>
<a class="sub" href="?<?=e(http_build_query(['area'=>$area,'lang'=>$lang,'menu_id'=>$selected]))?>">Odśwież wyniki</a>
</form>
<?php if($rows):?>
<div class="bulk flex"><label><input type="checkbox" id="v16-select-all"> Wybierz wszystkie na tej stronie</label><b id="v16-picked">0 wybranych</b><button type="button" id="v16-approve">Zapisz poprawki i zatwierdź zaznaczone</button></div>
<?php foreach($rows as $job):?>
<article class="job" data-v16-id="<?=(int)$job['id']?>">
<div class="flex"><label><input type="checkbox" class="v16-pick" value="<?=(int)$job['id']?>"> <strong><?=e(strtoupper((string)$job['lang']).' · '.$job['field'].' · #'.$job['entity_id'])?></strong></label>
<?php if($area==='blog' && !empty($job['record_name'])):?><span class="sub"><?=e(mb_strimwidth((string)$job['record_name'],0,85,'…','UTF-8'))?></span><?php endif;?>
</div>
<details><summary>Oryginał PL</summary><div class="original"><?=e((string)$job['source_text'])?></div></details>
<label class="sub">Wersja <?=e(strtoupper((string)$job['lang']))?> — można edytować</label>
<textarea class="v16-value <?=strlen((string)$job['translated_text'])>600?'long':''?>" spellcheck="true"><?=e((string)$job['translated_text'])?></textarea>
<?php if(function_exists('p14_missing_terms') && ($miss=p14_missing_terms((string)$job['source_text'],(string)$job['translated_text']))):?>
<p class="warnText" style="font-size:13px">Sprawdź chronione nazwy: <?=e(implode(', ',$miss))?></p>
<?php endif;?>
</article>
<?php endforeach;?>
<nav class="flex" aria-label="Strony wyników">
<?php if($page>1):?><a href="?<?=e(http_build_query($baseQuery+['page'=>$page-1]))?>">← Poprzednia</a><?php endif;?>
<span class="sub">Strona <?=$page?> · do 35 tłumaczeń na stronie</span>
<?php if(count($rows)===35):?><a href="?<?=e(http_build_query($baseQuery+['page'=>$page+1]))?>">Następna →</a><?php endif;?>
</nav>
<?php else:?><p class="sub">Brak gotowych tłumaczeń w wybranym filtrze. Jeżeli tłumaczenie właśnie trwa, odśwież listę po zakończeniu.</p><?php endif;?>
</section>
<section class="panel">
<details>
<summary style="cursor:pointer;font-weight:600">Dodatkowe narzędzia</summary>
<div class="flex"><a href="/admin/polana-v14-slownik.php">Słownik nazw własnych</a><a href="/admin/blog.php">Edytor bloga</a><a href="/admin/menus.php">Edytor menu</a><a href="/admin/polana-v11-podstrony.php">Kontrola SEO</a></div>
<p class="tiny">Starsze moduły V9–V15 pozostają jako zaplecze techniczne. Nie ma osobnej bazy tłumaczeń ani agenta Synology.</p>
</details>
</section>
</div>
<script>
(()=>{'use strict';
const endpoint='/admin/polana-v16-api.php',csrf=<?=json_encode(csrf_token())?>;
const current=<?=json_encode($area)?>,currentLang=<?=json_encode($lang)?>,currentMenu=<?=json_encode($selected)?>;
const menuIds=<?=json_encode(array_map('intval',array_keys($menus)))?>;
const pagePaths=<?=json_encode(array_keys($pagePaths),JSON_UNESCAPED_UNICODE)?>;
const sampleBlog=<?=json_encode($sampleBlog)?>;
const status=document.getElementById('v16-status'),progress=document.getElementById('v16-progress');
const buttons=[document.getElementById('v16-run'),document.getElementById('v16-all')],stop=document.getElementById('v16-stop');
let running=false,completed=0,failures=0,networkFailures=0,problemCount=0;
const delay=ms=>new Promise(resolve=>setTimeout(resolve,ms));
function say(text){status.textContent=text;}
function paint(area,c){for(const kind of ['pending','review','approved','errors']){const v=document.querySelector(`[data-counter="${area}-${kind}"]`);if(v&&c[kind]!==undefined)v.textContent=String(c[kind]);}}
function params(action,area,extra={}){const f=new FormData();f.append('csrf_token',csrf);f.append('action',action);f.append('area',area);f.append('menu_id',String(currentMenu));f.append('lang',currentLang);
for(const [key,value] of Object.entries(extra)){if(Array.isArray(value))value.forEach(v=>f.append(key+'[]',String(v)));else f.append(key,String(value));}return f;}
async function api(action,area,extra={}){
const response=await fetch(endpoint,{method:'POST',credentials:'same-origin',cache:'no-store',body:params(action,area,extra)});
let data;try{data=await response.json();}catch{throw new Error('Odpowiedź serwera nie jest JSON (HTTP '+response.status+'). Sprawdź logi PHP.');}
if(!response.ok||!data.ok)throw new Error(data.error||('HTTP '+response.status));
if(data.counts)paint(area,data.counts);return data;
}
function begin(){if(running)return false;running=true;completed=0;failures=0;networkFailures=0;problemCount=0;buttons.forEach(x=>x.disabled=true);stop.disabled=false;progress.style.display='block';progress.removeAttribute('value');return true;}
function end(){running=false;buttons.forEach(x=>x.disabled=false);stop.disabled=true;progress.style.display='none';}
function nextError(message,kind='content'){
failures++;problemCount++;
if(kind==='api')networkFailures++;else networkFailures=0;
say('Pomijam błędne zadanie ('+problemCount+'). '+message+' — przechodzę dalej.');
if(networkFailures>=3)throw new Error('Trzy kolejne błędy połączenia z API. Wstrzymano, aby nie obciążać serwera. Sprawdź diagnostykę poniżej.');
}
async function scanMenu(){say('Menu: sprawdzam aktualne karty…');const res=await api('scan_menu','menu');say('Menu: przygotowano nowe zadania '+(res.scanned?.created??0)+'.');}
async function tickMenu(){for(const id of menuIds){let n=0;while(running){const r=await api('tick_menu','menu',{menu_id:id});if(r.batch?.busy){await delay(1100);continue;}
if(r.batch?.empty || Number(r.menu_stats?.pending??0)===0)break;
if(r.batch?.error){
const b=r.batch;nextError('Menu #'+id+', zadanie #'+(b.job_id||'?')+', '+(b.lang||'')+': '+(b.detail||b.error),b.kind||'content');
}else{failures=0;networkFailures=0;}
completed++;n++;say('Menu #'+id+': przetworzono '+n+' fraz; do sprawdzenia '+r.menu_stats?.review+'; oczekuje '+r.menu_stats?.pending);await delay(420);
}if(!running)break;}}
async function scanBlog(){let offset=0,total=0;for(let rounds=0;rounds<1000&&running;rounds++){
const r=await api('scan_blog','blog',{offset});total+=(r.scan?.scanned??0);say('Blog: zeskanowano '+total+' wpisów…');
if(r.scan?.next===null || r.scan?.next===undefined)break;
const next=Number(r.scan.next);if(!Number.isFinite(next)||next<=offset)throw new Error('Błąd stronicowania wpisów');offset=next;
}say('Blog: zeskanowano '+total+' wpisów.');}
async function tickBlog(){let pauseRounds=0;let transientFailures=0;
while(running){const r=await api('tick_blog','blog');const b=r.batch||{},s=r.counts||{};
if(!b.attempted){
if(b.waiting && Number(s.pending??0)>0){
pauseRounds++;if(pauseRounds>5)throw new Error('API nadal zwraca HTTP 502. Wstrzymano po kilku przerwach — sprawdź Synology i Nginx. Zapisany postęp nie przepadł.');
for(let i=0;i<9&&running;i++){say('Blog: chwilowa przerwa po HTTP 502, wznowienie za około '+(90-i*10)+' s. Pozostało '+s.pending+' zadań.');await delay(10000);}continue;
}break;
}
if(b.failed){const problem=r.problems?.find(x=>Number(x.id)===Number(b.job_id))||r.problems?.[0];
const msg=problem?.message||'Błąd tłumaczenia wpisu';
if(/HTTP 502|HTTP 504|HTTP 429/i.test(msg)){
transientFailures++;problemCount++;
say('Blog #'+b.job_id+' ('+String(b.lang||'').toUpperCase()+'): '+msg+'. Próbuję innych zadań i później wrócę do tego.');
if(transientFailures>=3){transientFailures=0;pauseRounds++;if(pauseRounds>5)throw new Error('Powtarzający się HTTP 502/504/429. Sprawdź API Synology, zapisany postęp jest bezpieczny.');
for(let i=0;i<9&&running;i++){say('Synology/Proxy przeciążone. Wznawiam tłumaczenia za ok. '+(90-i*10)+' s.');await delay(10000);}}
}else nextError('Blog #'+(b.job_id||'?')+': '+msg,/HTTP |Polaczenie|timeout|cURL|SSL/i.test(msg)?'api':'content');
}else{failures=0;networkFailures=0;transientFailures=0;}
completed++;const fragment=b.partial?(' · wpis #'+b.entity_id+' '+String(b.lang||'').toUpperCase()+' '+b.field+' · fragment '+b.segment+'/'+b.segments):'';
say('Blog: '+completed+' żądań'+fragment+', oczekuje '+s.pending+', do akceptacji '+s.review);await delay(650);
}
}
async function scanPages(){let ok=0,skip=0;for(const path of pagePaths){if(!running)break;
const pathToFetch=path==='/blog-post'?sampleBlog:path;
if(!pathToFetch){skip++;continue;}
const url=pathToFetch+(pathToFetch.includes('?')?'&':'?')+'p12_scan='+encodeURIComponent(csrf);
try{const r=await fetch(url,{credentials:'same-origin',cache:'no-store',redirect:'follow'});
if(!r.ok||r.url.includes('/admin/login'))throw new Error('HTTP '+r.status);
await r.text();ok++;
}catch(e){skip++;}say('Podstrony: sprawdzono '+ok+', pominięto '+skip+', obecnie '+path);
}
if(skip)say('Podstrony: '+ok+' zeskanowanych, '+skip+' pominiętych; sprawdź pominięte podstrony i wymagania dostępu.');}
async function tickPages(){while(running){const r=await api('tick_pages','pages');if(r.busy){await delay(1200);continue;}
const b=r.batch||{},s=r.counts||{};if(!b.attempted || Number(s.pending??0)===0)break;
if(b.errors){const problem=r.problems?.[0];const msg=problem?.message||'Błąd tłumaczenia podstrony';
nextError('Podstrona, zadanie #'+(problem?.id||'?')+': '+msg,/HTTP |Polaczenie|timeout|cURL|SSL/i.test(msg)?'api':'content');
}else{failures=0;networkFailures=0;}
completed++;say('Podstrony: '+completed+' żądań, oczekuje '+s.pending+', do akceptacji '+s.review);await delay(600);
}}
async function runArea(a){failures=0;if(a==='menu'){await scanMenu();if(running)await tickMenu();}
else if(a==='blog'){await scanBlog();if(running)await tickBlog();}
else{await scanPages();if(running)await tickPages();}}
async function launch(areas){if(!begin())return;try{
for(const a of areas){if(!running)break;completed=0;say('Pracuję: '+(a==='menu'?'menu':a==='blog'?'blog':'podstrony')+'…');await runArea(a);}
if(running)say('Zakończono. Przetworzono '+completed+' żądań, błędne: '+problemCount+'. Odśwież stronę, by zatwierdzić wyniki; błędy można ponowić osobno.');
else say('Zatrzymano. Przetworzone tłumaczenia pozostają w MariaDB.');
}catch(err){say('Przerwano: '+err.message);}finally{end();}}
// V16.3: zatwierdzanie gotowych wyników w partiach 15 bez blokowania PHP.
const bulkPreviewButton=document.getElementById('v163-preview');
const bulkApproveButton=document.getElementById('v163-all-approve');
const bulkInfo=document.getElementById('v163-preview-results');
const bulkProgress=document.getElementById('v163-bulk-progress');
let bulkPreviewTotal=0,bulkWorking=false;
bulkPreviewButton.addEventListener('click',async()=>{
if(running||bulkWorking)return;
bulkApproveButton.disabled=true;
bulkInfo.style.display='block';bulkInfo.textContent='Sprawdzam gotowe pozycje…';
try{
const r=await api('bulk_preview',current,{bulk_menu_id:0});
bulkPreviewTotal=Number(r.preview?.total??0);
const examples=r.preview?.examples||[];
const lines=examples.map(x=>'#'+x.id+' '+x.lang.toUpperCase()+' / '+x.field+'\nPL: '+x.source+'\nTL: '+x.translated);
bulkInfo.textContent='Gotowych w filtrze: '+bulkPreviewTotal+'.\n'+
(current==='menu'?'Menu: wszystkie aktualne karty (Dania, Napoje, Wina).\n':'')+
(examples.length?'Przykłady (pierwsze '+examples.length+'):\n\n'+lines.join('\n\n'):'Brak tłumaczeń do zatwierdzenia.');
bulkApproveButton.disabled=bulkPreviewTotal===0;
}catch(err){bulkInfo.textContent='Nie udało się sprawdzić: '+err.message;}
});
bulkApproveButton.addEventListener('click',async()=>{
if(running||bulkWorking||!bulkPreviewTotal)return;
if([...document.querySelectorAll('.v16-pick:checked')].some(el=>{
const t=el.closest('.job')?.querySelector('textarea');return t && t.value!==t.defaultValue;
}))return alert('Masz niezapisane poprawki w widocznych pozycjach. Najpierw kliknij „Zapisz poprawki i zatwierdź zaznaczone”.');
if(!confirm('Zatwierdzić zbiorczo do '+bulkPreviewTotal+' GOTOWYCH tłumaczeń z wybranego modułu i języka?\n\nTo publikuje wyniki, których nie przeglądano pojedynczo. Błędy nazw własnych i nieaktualne teksty zostaną pominięte.'))return;
bulkWorking=true;bulkApproveButton.disabled=true;bulkPreviewButton.disabled=true;
let cursor=0,done=0,reviewed=0,skipped=0,examples=[];
try{
for(let cycle=0;cycle<1000;cycle++){
const r=await api('bulk_next',current,{bulk_menu_id:0,cursor,acknowledge:'ZATWIERDZAM'});
const b=r.batch||{};
done+=Number(b.approved??0);reviewed+=Number(b.processed??0);
const fail=b.skipped||[];skipped+=fail.length;
for(const f of fail){if(examples.length<8)examples.push('#'+f.id+' '+f.reason);}
const next=Number(b.cursor??cursor);
if(next<cursor || (next===cursor&&!b.finished))throw new Error('Nieprawidłowy postęp zatwierdzania');
cursor=next;
bulkProgress.textContent='Zatwierdzono '+done+' / sprawdzono '+reviewed+'; pominięto '+skipped+'. Pracuję dalej…';
if(b.finished||Number(b.processed??0)===0)break;
await delay(180);
}
bulkProgress.textContent='Zakończono: zatwierdzono '+done+', pominięto '+skipped+'.'+
(examples.length?' Do ręcznego sprawdzenia: '+examples.join('; '):'');
bulkInfo.textContent='Pozycje pominięte pozostają w kolejce do ręcznej korekty. Odśwież stronę, aby zobaczyć aktualne liczniki.';
bulkPreviewTotal=0;
}catch(err){bulkProgress.textContent='Przerwano po zatwierdzeniu '+done+' pozycji: '+err.message+' Można wznowić po ponownym sprawdzeniu.';}
finally{bulkWorking=false;bulkPreviewButton.disabled=false;}
});
const errorPane=document.getElementById('v16-errors');
async function showProblems(){
try{
const result=await api('diagnostics',current);const rows=result.problems||[];
errorPane.replaceChildren();
if(!rows.length){errorPane.textContent='Brak zapisanych błędów w wybranym module / karcie.';return;}
const heading=document.createElement('p');heading.textContent='Ostatnie błędy ('+rows.length+' pozycji):';errorPane.appendChild(heading);
for(const row of rows){
const item=document.createElement('div');item.style.cssText='padding:9px;margin:6px 0;background:#fff5e8;border:1px solid #e6bd80;border-radius:7px;white-space:pre-wrap;overflow-wrap:anywhere';
item.textContent='#'+row.id+' · '+row.lang.toUpperCase()+' · próby: '+row.attempts+' · '+row.item+'\n'+row.message+'\nPL: '+row.source_text;
errorPane.appendChild(item);
}
}catch(err){errorPane.textContent='Nie udało się pobrać diagnostyki: '+err.message;}
}
document.getElementById('v16-check-errors').addEventListener('click',showProblems);
document.getElementById('v16-retry-errors').addEventListener('click',async()=>{
if(running)return alert('Najpierw zatrzymaj tłumaczenie.');
if(!confirm('Ponowić tylko zadania, które wykorzystały limit prób? Zatwierdzone tłumaczenia pozostają bez zmian.'))return;
try{const result=await api('retry_failed',current);say('Przywrócono do kolejki '+result.reset+' błędnych zadań. Możesz uruchomić tłumaczenie.');await showProblems();}
catch(err){alert('Nie udało się ponowić: '+err.message);}
});
buttons[0].addEventListener('click',()=>launch([current]));
buttons[1].addEventListener('click',()=>launch(['menu','blog','pages']));
stop.addEventListener('click',()=>{running=false;say('Zatrzymywanie po bieżącym żądaniu…');});
const force=document.getElementById('v16-force');if(force)force.addEventListener('click',async()=>{
if(running)return;
const languages=[...document.querySelectorAll('#v16-force-languages input:checked')].map(el=>el.value);
if(!languages.length)return alert('Wybierz EN, DE lub ES.');
if(!confirm('Ponowić tłumaczenie całej wybranej karty w językach '+languages.join(', ')+'? Dotychczasowa publikacja pozostaje do zatwierdzenia nowych tekstów.'))return;
try{const r=await api('scan_menu_force','menu',{languages,confirm:'PONOW'});say('Przygotowano '+(r.scan?.created??0)+' nowych zadań. Kliknij „Tłumacz menu”.');}
catch(err){alert(err.message);}
});
const boxes=[...document.querySelectorAll('.v16-pick')],select=document.getElementById('v16-select-all');
function count(){if(!select)return;const n=boxes.filter(x=>x.checked).length;document.getElementById('v16-picked').textContent=n+' wybranych';select.checked=n>0&&n===boxes.length;select.indeterminate=n>0&&n<boxes.length;}
if(select){select.addEventListener('change',()=>{boxes.forEach(b=>b.checked=select.checked);count();});boxes.forEach(b=>b.addEventListener('change',count));}
const approve=document.getElementById('v16-approve');if(approve)approve.addEventListener('click',async()=>{
if(running)return alert('Zatrzymaj najpierw proces tłumaczenia.');
const selected=boxes.filter(x=>x.checked).map(x=>x.value);
if(!selected.length)return alert('Zaznacz pozycje do zatwierdzenia.');
if(!confirm('Zapisz własne korekty i zatwierdź '+selected.length+' wybranych tłumaczeń?'))return;
const form=params('approve',current);for(const id of selected){const card=document.querySelector('[data-v16-id="'+id+'"]');
form.append('ids[]',id);form.append('values['+id+']',card.querySelector('textarea').value);
}
approve.disabled=true;try{
const response=await fetch(endpoint,{method:'POST',body:form,credentials:'same-origin',cache:'no-store'});const r=await response.json();
if(!r.ok)throw new Error(r.error||'HTTP '+response.status);
const result=r.result||{},failed=result.failed||[];
alert('Zatwierdzono '+result.approved+' z '+selected.length+' pozycji.'+(failed.length?' Błędy: '+failed.slice(0,3).map(x=>'#'+x.id+' '+x.reason).join('; '):''));
location.reload();
}catch(err){alert('Nie udało się zatwierdzić: '+err.message);}finally{approve.disabled=false;}
});
})();
</script>
<?php render_admin_footer(); ?>