| 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';
$user=require_blog_manager();
ensure_blog_schema();
const POLANA_BATCH='polana_blog40_20261017';
function pb40_schema(): void {
db()->exec("CREATE TABLE IF NOT EXISTS polana_blog_campaign_items (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
campaign_key VARCHAR(64) NOT NULL,
ordinal SMALLINT UNSIGNED NOT NULL,
post_id BIGINT UNSIGNED NOT NULL,
reviewed_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_campaign_order(campaign_key,ordinal),
UNIQUE KEY uq_campaign_post(campaign_key,post_id),
KEY ix_post(post_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_polish_ci");
}
function pb40_rows(): array {
$stmt=db()->prepare("SELECT x.ordinal,x.post_id,x.reviewed_at,p.title,p.slug,p.category,p.status,p.publish_at
FROM polana_blog_campaign_items x LEFT JOIN blog_posts p ON p.id=x.post_id
WHERE x.campaign_key=? ORDER BY x.ordinal");
$stmt->execute([POLANA_BATCH]);return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function pb40_selected(): array {
$ids=$_POST['selected']??[];
if(!is_array($ids))throw new RuntimeException('Nieprawidłowa lista zaznaczeń.');
$ids=array_values(array_unique(array_filter(array_map('intval',$ids),static fn(int $n): bool=>$n>0)));
if(count($ids)>40)throw new RuntimeException('Zaznacz maksymalnie 40 pozycji.');
return $ids;
}
function pb40_allowed(array $ids): array {
if(!$ids)return [];
$placeholders=implode(',',array_fill(0,count($ids),'?'));
$stmt=db()->prepare("SELECT x.ordinal,x.post_id,x.reviewed_at,p.status,p.title,p.content
FROM polana_blog_campaign_items x INNER JOIN blog_posts p ON p.id=x.post_id
WHERE x.campaign_key=? AND x.post_id IN ($placeholders) ORDER BY x.ordinal");
$stmt->execute(array_merge([POLANA_BATCH],$ids));return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
function pb40_schedule_time(int $offset): string {
$start=new DateTimeImmutable('next monday 10:00',new DateTimeZone('Europe/Warsaw'));
return $start->modify('+'.intdiv($offset,3).' weeks')->modify('+'.([0,2,4][$offset%3]).' days')->format('Y-m-d H:i:s');
}
pb40_schema();
if($_SERVER['REQUEST_METHOD']==='POST'){
verify_csrf();
try{
$action=(string)($_POST['action']??'');
$db=db();
if($action==='import'){
$data=require dirname(__DIR__).'/includes/polana_blog40_v10_data.php';
if(count($data)!==40)throw new RuntimeException('Nieprawidłowa paczka artykułów.');
$db->beginTransaction();
try{
$imported=0;$skipped=0;
$check=$db->prepare('SELECT id FROM blog_posts WHERE slug=? LIMIT 1');
$insert=$db->prepare("INSERT INTO blog_posts
(title,slug,category,excerpt,content,status,publish_at,seo_title,seo_description,created_by_user_id,updated_by_user_id)
VALUES (?,?,?,?,?,'draft',NULL,?,?,?,?)");
$link=$db->prepare("INSERT INTO polana_blog_campaign_items(campaign_key,ordinal,post_id)
VALUES (?,?,?) ON DUPLICATE KEY UPDATE post_id=post_id");
foreach($data as $article){
$slug=(string)$article['slug'];
$check->execute([$slug]);$found=$check->fetchColumn();
if($found){$skipped++;continue;}
$insert->execute([$article['title'],$slug,$article['category'],$article['excerpt'],
$article['content'],$article['seo_title'],$article['seo_description'],(int)$user['id'],(int)$user['id']]);
$link->execute([POLANA_BATCH,(int)$article['ordinal'],(int)$db->lastInsertId()]);$imported++;
}
$db->commit();
admin_log('polana_blog40_import','Zaimportowano '.$imported.' wpisów, pominięto '.$skipped);
flash('success','Dodano '.$imported.' szkiców. Istniejących slugów nie nadpisano (pominięto: '.$skipped.').');
}catch(Throwable $e){$db->rollBack();throw $e;}
} else {
$ids=pb40_selected();if(!$ids)throw new RuntimeException('Zaznacz wpisy w tabeli.');
$rows=pb40_allowed($ids);if(count($rows)!==count($ids))throw new RuntimeException('Nie wszystkie wybrane wpisy należą do paczki.');
if($action==='review'){
$q=$db->prepare('UPDATE polana_blog_campaign_items SET reviewed_at=NOW() WHERE campaign_key=? AND post_id=?');
foreach($rows as $row)$q->execute([POLANA_BATCH,(int)$row['post_id']]);
flash('success','Oznaczono do publikacji po przeglądzie: '.count($rows));
// V19.3: reviewed campaign items go straight into the next available schedule slots.
try {
require_once dirname(__DIR__).'/includes/polana_blog_scheduler_v193.php';
$planned=p193_schedule_reviewed_campaign(array_column($rows,'post_id'),(int)$user['id']);
flash('success','Automatycznie zaplanowano '.$planned['count'].' sprawdzonych wpisów.');
} catch (Throwable $autoError) {
error_log('POLANA V19.3 campaign: '.$autoError->getMessage());
flash('error','Teksty zatwierdzono, ale nie wpisano do harmonogramu. Zajrzyj do V19.3.');
}
}elseif($action==='publish'){
if(count($rows)>4)throw new RuntimeException('Jednorazowo publikujemy maksymalnie 4 wpisy.');
$q=$db->prepare("UPDATE blog_posts SET status='published',publish_at=NOW(),updated_by_user_id=?,updated_at=NOW() WHERE id=? AND status='draft'");
$n=0;
foreach($rows as $r){if(empty($r['reviewed_at'])||$r['status']!=='draft')throw new RuntimeException('Wybrane wpisy muszą być sprawdzonymi szkicami.');}
foreach($rows as $r){$q->execute([(int)$user['id'],(int)$r['post_id']]);$n+=$q->rowCount();}
admin_log('polana_blog40_publish','Opublikowano '.$n.' wybranych artykułów');
flash('success','Opublikowano '.$n.' wpisów PL. Zagraniczne wersje pojawią się dopiero po zatwierdzeniu tłumaczeń.');
}elseif($action==='schedule'){
foreach($rows as $r){if(empty($r['reviewed_at'])||$r['status']!=='draft')throw new RuntimeException('Zaplanuj tylko zatwierdzone szkice.');}
$q=$db->prepare("UPDATE blog_posts SET status='scheduled',publish_at=?,updated_by_user_id=?,updated_at=NOW() WHERE id=? AND status='draft'");
$n=0;foreach($rows as $i=>$r){$q->execute([pb40_schedule_time($i),(int)$user['id'],(int)$r['post_id']]);$n+=$q->rowCount();}
admin_log('polana_blog40_schedule','Zaplanowano '.$n.' artykułów (3 tygodniowo)');
flash('success','Zaplanowano '.$n.' artykułów: poniedziałki, środy i piątki o 10:00.');
}elseif($action==='queue'){
$file=dirname(__DIR__).'/includes/tklm_v9.php';
if(!is_file($file))throw new RuntimeException('Brak modułu tłumaczeń V9.');
require_once $file;
if(!function_exists('v9_enqueue_blog_post'))throw new RuntimeException('Brak funkcji kolejki bloga. Zainstaluj V9.2.');
v9_init();$n=0;
foreach($rows as $r){$n+=v9_enqueue_blog_post((int)$r['post_id']);}
flash('success','Dodano do kolejki '.$n.' pól językowych. Uruchom tłumaczenie w Centrum V9.5.');
}else{throw new RuntimeException('Nieznana operacja.');}
}
redirect('/admin/polana-blog-40.php');
}catch(Throwable $e){flash('error',$e->getMessage());redirect('/admin/polana-blog-40.php');}
}
$rows=pb40_rows();
$have=count($rows);$draft=0;$sched=0;$pub=0;$checked=0;
foreach($rows as $r){if($r['status']==='draft')$draft++;if($r['status']==='scheduled')$sched++;if($r['status']==='published')$pub++;if($r['reviewed_at'])$checked++;}
render_admin_header('Kampania 40 artykułów SEO');
?>
<style>
.pb40-wrap{max-width:1240px;margin:0 auto;padding:20px}.pb40-hero{padding:24px;border-radius:16px;background:#f4f6f1;color:#202720;margin-bottom:18px}.pb40-stats{display:flex;flex-wrap:wrap;gap:14px}.pb40-stats b{font-size:24px;display:block}.pb40-stats div{border:1px solid #cbd4c8;border-radius:9px;background:#fff;padding:12px 18px}.pb40-actions{display:flex;flex-wrap:wrap;gap:10px;margin:16px 0}.pb40-actions button{border:0;border-radius:8px;background:#27452d;color:white;padding:12px 14px;cursor:pointer}.pb40-actions button:focus-visible{outline:3px solid orange}.pb40-actions .secondary{background:#e0e8db;color:#182917}.pb40-table{width:100%;border-collapse:collapse}.pb40-table th,.pb40-table td{padding:11px;border-bottom:1px solid #ddd;vertical-align:top;text-align:left}.pb40-table td small{display:block;color:#666}.pb40-note{background:#fff7e3;border:1px solid #ecd69d;border-radius:10px;padding:14px;margin:16px 0}.pb40-scroller{overflow-x:auto}@media(max-width:600px){.pb40-wrap{padding:8px}.pb40-table{font-size:13px}}
</style>
<main class="pb40-wrap">
<div class="pb40-hero"><h1>Blog SEO — 40 artykułów</h1>
<p>Gotowe teksty PL o Kazimierzu, grzybach, dyni i kuchni polskiej. Import nie nadpisuje istniejących wpisów. Publikacja jest osobnym, świadomym działaniem po sprawdzeniu.</p>
<div class="pb40-stats">
<div><b><?= $have ?></b>Zaimportowanych</div><div><b><?= $draft ?></b>Szkiców</div><div><b><?= $checked ?></b>Sprawdzonych</div>
<div><b><?= $sched ?></b>Zaplanowanych</div><div><b><?= $pub ?></b>Opublikowanych</div>
</div></div>
<?php if(setting('nav_blog_enabled','0')!=='1'): ?>
<div class="pb40-note"><b>Blog jest wyłączony publicznie.</b> Wpisy nie będą widoczne, dopóki nie włączysz modułu Blog w <a href="/admin/modules.php">Sekcjach strony</a>.</div>
<?php endif; ?>
<?php if($have<40): ?>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="import">
<button style="padding:14px 22px;background:#2c6535;color:#fff;border:0;border-radius:8px;font-weight:bold">Importuj 40 artykułów jako szkice</button>
<p>Import można ponawiać — istniejących adresów nie rusza.</p></form>
<?php endif; ?>
<div class="pb40-note">Najpierw przeczytaj i popraw artykuły, potem zaznacz je i wybierz <b>Oznacz jako sprawdzone</b>. Tłumaczenia EN/DE/ES zapisują się przez istniejącą kolejkę V9.5 i wymagają osobnej akceptacji. Harmonogram działa przez wejścia na blog, bez crona. Teksty nie zawierają zdjęć; dodasz je w edytorze.</div>
<form method="post" id="pb40-form"><?= csrf_field() ?>
<div class="pb40-actions">
<button type="button" id="pb40-first4" class="secondary">Zaznacz pierwsze 4</button>
<button type="button" id="pb40-next36" class="secondary">Zaznacz pozostałe 36</button>
<button type="button" id="pb40-clear" class="secondary">Odznacz</button>
<span id="pb40-total" style="align-self:center;font-weight:600">0 zaznaczonych</span>
</div><div class="pb40-actions">
<button type="submit" name="action" value="review" class="secondary" onclick="return confirm('Potwierdzasz, że przeczytano i sprawdzono wybrane artykuły?')">✓ Oznacz sprawdzone</button>
<button type="submit" name="action" value="publish" onclick="return confirm('Opublikować do 4 zaznaczonych, sprawdzonych szkiców TERAZ?')">Publikuj wybrane (max 4)</button>
<button type="submit" name="action" value="schedule" onclick="return confirm('Zaplanować wybrane, sprawdzone szkice 3 razy w tygodniu?')">Zaplanuj wybrane: pn / śr / pt</button>
<button type="submit" name="action" value="queue" class="secondary" onclick="return confirm('Dodać EN/DE/ES dla zaznaczonych artykułów do kolejki API?')">Tłumacz wybrane EN/DE/ES</button>
<a class="btn btn-light" href="/admin/v9-review.php">Centrum tłumaczeń V9.5 →</a>
<a class="btn btn-light" href="/admin/blog.php">Edytor bloga →</a>
</div>
<div class="pb40-scroller"><table class="pb40-table"><thead><tr><th><input type="checkbox" id="pb40-all" aria-label="Zaznacz wszystkie"></th><th>Artykuł</th><th>Status</th><th>Kontrola</th><th>Podgląd</th></tr></thead><tbody>
<?php foreach($rows as $r): ?>
<tr><td><input type="checkbox" class="pb40-check" data-ordinal="<?= (int)$r['ordinal'] ?>" name="selected[]" value="<?= (int)$r['post_id'] ?>"></td>
<td><b><?= e((string)$r['title']) ?></b><small>#<?= (int)$r['ordinal'] ?> • <?= e((string)$r['category']) ?></small></td>
<td><?= e((string)($r['status']??'brak')) ?><?php if($r['publish_at']): ?><small><?= e((string)$r['publish_at']) ?></small><?php endif; ?></td>
<td><?= $r['reviewed_at']?'✓ sprawdzony':'do sprawdzenia' ?></td>
<td><a href="/admin/blog-edit.php?id=<?= (int)$r['post_id'] ?>">Edytuj</a> · <a href="/blog/<?= e((string)$r['slug']) ?>?preview=1" target="_blank" rel="noopener">Podgląd</a></td></tr>
<?php endforeach; ?>
</tbody></table></div></form>
<?php if(!$rows): ?><p>Po imporcie w tym miejscu pojawi się lista 40 artykułów.</p><?php endif; ?>
</main>
<script>(()=>{const all=document.querySelector('#pb40-all'),list=[...document.querySelectorAll('.pb40-check')];if(!all)return;all.addEventListener('change',()=>list.forEach(el=>el.checked=all.checked));const count=document.querySelector('#pb40-total');const update=()=>{all.checked=list.length>0&&list.every(i=>i.checked);all.indeterminate=list.some(i=>i.checked)&&!all.checked;count.textContent=list.filter(i=>i.checked).length+' zaznaczonych';};list.forEach(el=>el.addEventListener('change',update));for(const [btn,test] of [['pb40-first4',n=>n<=4],['pb40-next36',n=>n>4],['pb40-clear',()=>false]])document.getElementById(btn)?.addEventListener('click',()=>{list.forEach(el=>el.checked=test(Number(el.dataset.ordinal)));update();});})();</script>
<?php render_admin_footer(); ?>