Add BackPRO News theme and update database schema for article tracking

- Introduced a new WordPress theme "BackPRO News" with a lightweight magazine-style design.
- Added columns for tracking retry attempts and timestamps for unpublished/generated articles in the articles table.
- Included remote service metadata fields in the sites table for better management.
- Created log files for image replacements, installer actions, OpenAI article generation, and publishing processes.
- Implemented a dashboard template for site management, including permalink settings and theme installation options.
This commit is contained in:
2026-02-17 20:08:02 +01:00
parent b653cea252
commit 4d5e220b3c
41 changed files with 4229 additions and 239 deletions

View File

@@ -24,13 +24,13 @@ class PublisherService
public function publishNext(): array
{
Logger::info('Rozpoczynam automatyczną publikację', 'publish');
Logger::info('Rozpoczynam automatyczna publikacje', 'publish');
$sites = Site::findDueForPublishing();
if (empty($sites)) {
Logger::info('Brak stron do publikacji', 'publish');
return ['success' => false, 'message' => 'Brak stron wymagających publikacji.'];
return ['success' => false, 'message' => 'Brak stron wymagajacych publikacji.'];
}
$site = $sites[0];
@@ -41,19 +41,42 @@ class PublisherService
{
Logger::info("Publikacja dla strony: {$site['name']} (ID: {$site['id']})", 'publish');
// 1. Select topic
// 1. Najpierw publikuj gotowe, nieopublikowane artykuly.
$retryArticle = Article::findNextRetryableBySite((int) $site['id']);
if ($retryArticle) {
$topic = Topic::find((int) $retryArticle['topic_id']);
if (!$topic) {
Logger::error("Nie znaleziono tematu dla artykulu ID {$retryArticle['id']}", 'publish');
return ['success' => false, 'message' => 'Nie znaleziono tematu dla oczekujacego artykulu.'];
}
Logger::info("Ponowna proba publikacji artykulu ID {$retryArticle['id']}: {$retryArticle['title']}", 'publish');
Article::markRetryAttempt((int) $retryArticle['id']);
return $this->publishPreparedArticle(
$site,
$topic,
[
'title' => (string) $retryArticle['title'],
'content' => (string) $retryArticle['content'],
'model' => $retryArticle['ai_model'] ?? null,
'prompt' => $retryArticle['prompt_used'] ?? null,
],
(int) $retryArticle['id']
);
}
// 2. Gdy brak zaleglych, generuj nowy artykul.
$topic = $this->topicBalancer->getNextTopic($site['id']);
if (!$topic) {
Logger::error("Brak aktywnych tematów dla strony {$site['name']}", 'publish');
return ['success' => false, 'message' => "Brak aktywnych tematów dla strony {$site['name']}."];
Logger::error("Brak aktywnych tematow dla strony {$site['name']}", 'publish');
return ['success' => false, 'message' => "Brak aktywnych tematow dla strony {$site['name']}."];
}
Logger::info("Wybrany temat: {$topic['name']} (ID: {$topic['id']})", 'publish');
// 2. Get existing titles to avoid repetition
$existingTitles = Article::getRecentTitlesByTopic($topic['id'], 20);
$existingTitles = Article::getRecentTitlesByTopic((int) $topic['id'], 20);
// 3. Generate article
$article = $this->openAI->generateArticle(
$topic['name'],
$topic['description'] ?? '',
@@ -61,16 +84,22 @@ class PublisherService
);
if (!$article) {
$this->saveFailedArticle($site, $topic, 'Nie udało się wygenerować artykułu przez OpenAI.');
return ['success' => false, 'message' => 'Błąd generowania artykułu przez AI.'];
$this->saveFailedArticle($site, $topic, 'Nie udalo sie wygenerowac artykulu przez OpenAI.');
return ['success' => false, 'message' => 'Blad generowania artykulu przez AI.'];
}
Logger::info("Wygenerowano artykuł: {$article['title']}", 'publish');
Logger::info("Wygenerowano artykul: {$article['title']}", 'publish');
// 4. Generate/fetch image
$article['title'] = $this->normalizeArticleTitle((string) ($article['title'] ?? ''), (string) $topic['name']);
return $this->publishPreparedArticle($site, $topic, $article);
}
private function publishPreparedArticle(array $site, array $topic, array $article, ?int $existingArticleId = null): array
{
$imageUrl = null;
$mediaId = null;
$image = $this->imageService->generate($article['title'], $topic['name']);
$image = $this->imageService->generate((string) $article['title'], (string) $topic['name']);
if ($image) {
$mediaId = $this->wordpress->uploadMedia($site, $image['data'], $image['filename']);
@@ -78,42 +107,59 @@ class PublisherService
Logger::info("Upload obrazka: media_id={$mediaId}", 'publish');
}
} else {
Logger::warning('Nie udało się wygenerować obrazka, publikacja bez obrazka', 'publish');
Logger::warning('Nie udalo sie wygenerowac obrazka, publikacja bez obrazka', 'publish');
}
// 5. Publish to WordPress
$wpPostId = $this->wordpress->createPost(
$site,
$article['title'],
$article['content'],
(string) $article['title'],
(string) $article['content'],
$topic['wp_category_id'],
$mediaId
);
if (!$wpPostId) {
$this->saveFailedArticle($site, $topic, 'Nie udało się opublikować posta na WordPress.', $article);
return ['success' => false, 'message' => 'Błąd publikacji na WordPress.'];
$this->saveFailedArticle(
$site,
$topic,
'Nie udalo sie opublikowac posta na WordPress.',
$article,
$existingArticleId
);
return ['success' => false, 'message' => 'Blad publikacji na WordPress.'];
}
Logger::info("Opublikowano post: wp_post_id={$wpPostId}", 'publish');
// 6. Save article in database
Article::create([
'site_id' => $site['id'],
'topic_id' => $topic['id'],
'title' => $article['title'],
'content' => $article['content'],
'wp_post_id' => $wpPostId,
'image_url' => $imageUrl,
'status' => 'published',
'ai_model' => $article['model'],
'prompt_used' => $article['prompt'],
'published_at' => date('Y-m-d H:i:s'),
]);
if ($existingArticleId !== null) {
Article::update($existingArticleId, [
'title' => (string) $article['title'],
'content' => (string) $article['content'],
'wp_post_id' => $wpPostId,
'image_url' => $imageUrl,
'status' => 'published',
'ai_model' => $article['model'] ?? null,
'prompt_used' => $article['prompt'] ?? null,
'error_message' => null,
'published_at' => date('Y-m-d H:i:s'),
]);
} else {
Article::create([
'site_id' => $site['id'],
'topic_id' => $topic['id'],
'title' => (string) $article['title'],
'content' => (string) $article['content'],
'wp_post_id' => $wpPostId,
'image_url' => $imageUrl,
'status' => 'published',
'ai_model' => $article['model'] ?? null,
'prompt_used' => $article['prompt'] ?? null,
'published_at' => date('Y-m-d H:i:s'),
]);
}
// 7. Update counters
Topic::incrementArticleCount($topic['id']);
Site::updateLastPublished($site['id']);
Topic::incrementArticleCount((int) $topic['id']);
Site::updateLastPublished((int) $site['id']);
$message = "Opublikowano: \"{$article['title']}\" na {$site['name']}";
Logger::info($message, 'publish');
@@ -121,18 +167,51 @@ class PublisherService
return ['success' => true, 'message' => $message];
}
private function saveFailedArticle(array $site, array $topic, string $error, ?array $article = null): void
private function normalizeArticleTitle(string $title, string $topicName): string
{
Article::create([
'site_id' => $site['id'],
'topic_id' => $topic['id'],
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
'content' => $article['content'] ?? '',
'status' => 'failed',
'ai_model' => $article['model'] ?? null,
'prompt_used' => $article['prompt'] ?? null,
'error_message' => $error,
]);
$title = trim($title);
$topicName = trim($topicName);
if ($title === '' || $topicName === '') {
return $title;
}
// Remove "<topic name>: " / "<topic name> - " prefixes from generated titles.
$topicPattern = preg_quote($topicName, '/');
$normalized = preg_replace('/^' . $topicPattern . '\s*[:\-\x{2013}\x{2014}|]\s*/iu', '', $title);
$normalized = is_string($normalized) ? trim($normalized) : $title;
return $normalized !== '' ? $normalized : $title;
}
private function saveFailedArticle(
array $site,
array $topic,
string $error,
?array $article = null,
?int $existingArticleId = null
): void {
if ($existingArticleId !== null) {
Article::update($existingArticleId, [
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
'content' => $article['content'] ?? '',
'status' => 'failed',
'ai_model' => $article['model'] ?? null,
'prompt_used' => $article['prompt'] ?? null,
'error_message' => $error,
]);
} else {
Article::create([
'site_id' => $site['id'],
'topic_id' => $topic['id'],
'title' => $article['title'] ?? 'FAILED - nie wygenerowano',
'content' => $article['content'] ?? '',
'status' => 'failed',
'ai_model' => $article['model'] ?? null,
'prompt_used' => $article['prompt'] ?? null,
'error_message' => $error,
]);
}
Logger::error("Publikacja nieudana: {$error}", 'publish');
}