feat(admin): always-on dashboard widget + default retro days = 0

- Dashboard widget is now registered unconditionally. Header + content
  adapt: shows '⚠️ ошибки доставки' only when failed > 0, otherwise
  plain title with queue state (pending / sent-24h / failed).
  Pilot feedback: the conditional widget felt like the plugin was
  missing — users expect a persistent status readout.
- Retro default window changed from 7 days to 0 (no limit). Most
  pilot uses were 'dump everything since install', so 0 is the more
  useful default. User can still narrow to N days in the form.
- Spec §10 + §14 updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vladimir Bryzgalov
2026-04-18 00:12:45 +05:00
parent 2de8f0be57
commit dbdfc9afa9
6 changed files with 103 additions and 43 deletions
+12 -6
View File
@@ -7,15 +7,18 @@ final class Dashboard_Widget
{ {
public function register(): void public function register(): void
{ {
add_action('wp_dashboard_setup', [$this, 'maybe_register_widget']); add_action('wp_dashboard_setup', [$this, 'register_widget']);
} }
public function maybe_register_widget(): void public function register_widget(): void
{ {
if (Queue::count_failed() === 0) return; $failed_count = Queue::count_failed();
$title = $failed_count > 0
? '⚠️ CF7 Spam → Telegram: ошибки доставки'
: 'CF7 Spam → Telegram';
wp_add_dashboard_widget( wp_add_dashboard_widget(
'cf7stg_dashboard_errors', 'cf7stg_dashboard_status',
'⚠️ CF7 Spam → Telegram: ошибки доставки', $title,
[$this, 'render'] [$this, 'render']
); );
} }
@@ -23,7 +26,10 @@ final class Dashboard_Widget
public function render(): void public function render(): void
{ {
$failed = Queue::list_failed(10); $failed = Queue::list_failed(10);
$count = Queue::count_failed(); $failed_count = Queue::count_failed();
$pending = Queue::count_by_status(Queue::STATUS_PENDING);
$sent_24h = Queue::count_sent_last_24h();
$configured = Settings::is_configured();
$settings_url = admin_url('options-general.php?page=cf7stg-settings'); $settings_url = admin_url('options-general.php?page=cf7stg-settings');
include CF7STG_PLUGIN_DIR . 'admin/views/dashboard-widget.php'; include CF7STG_PLUGIN_DIR . 'admin/views/dashboard-widget.php';
} }
+1 -1
View File
@@ -72,7 +72,7 @@ final class Settings_Page
{ {
$this->guard('cf7stg_retro'); $this->guard('cf7stg_retro');
$count = max(1, min(RetroImporter::MAX_COUNT, (int)($_POST['retro_count'] ?? 10))); $count = max(1, min(RetroImporter::MAX_COUNT, (int)($_POST['retro_count'] ?? 10)));
$days = max(0, (int)($_POST['retro_days'] ?? 7)); $days = max(0, (int)($_POST['retro_days'] ?? 0));
$repeat = !empty($_POST['retro_repeat']); $repeat = !empty($_POST['retro_repeat']);
$result = RetroImporter::import($count, $days, $repeat); $result = RetroImporter::import($count, $days, $repeat);
$this->redirect_back(['retro_added' => (int)$result['added'], 'retro_skipped' => (int)$result['skipped']]); $this->redirect_back(['retro_added' => (int)$result['added'], 'retro_skipped' => (int)$result['skipped']]);
+22 -2
View File
@@ -1,9 +1,27 @@
<?php <?php
/** @var array<int,object> $failed */ /** @var array<int,object> $failed */
/** @var int $count */ /** @var int $failed_count */
/** @var int $pending */
/** @var int $sent_24h */
/** @var bool $configured */
/** @var string $settings_url */ /** @var string $settings_url */
?> ?>
<p>Есть <b><?php echo (int)$count; ?></b> неотправленных сообщений (TG-API отвечает ошибкой):</p> <?php if (!$configured): ?>
<p>⚙️ Плагин не настроен — укажите <b>Bot Token</b> и <b>Chat ID</b> в настройках.</p>
<?php endif; ?>
<p>
<span title="Ожидают отправки">⏳ В очереди: <b><?php echo (int)$pending; ?></b></span>
&nbsp;·&nbsp;
<span title="Успешно отправлено за последние сутки">✅ За сутки: <b><?php echo (int)$sent_24h; ?></b></span>
&nbsp;·&nbsp;
<span title="Не удалось доставить после ретраев">
<?php echo $failed_count > 0 ? '⚠️' : '✖'; ?> Ошибки: <b><?php echo (int)$failed_count; ?></b>
</span>
</p>
<?php if ($failed_count > 0): ?>
<p style="margin-top:10px">Последние ошибки доставки (TG-API):</p>
<ul style="margin:0 0 8px 18px;list-style:disc"> <ul style="margin:0 0 8px 18px;list-style:disc">
<?php foreach ($failed as $row): ?> <?php foreach ($failed as $row): ?>
<li> <li>
@@ -12,4 +30,6 @@
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?>
<p><a href="<?php echo esc_url($settings_url); ?>" class="button">Открыть настройки →</a></p> <p><a href="<?php echo esc_url($settings_url); ?>" class="button">Открыть настройки →</a></p>
+1 -1
View File
@@ -83,7 +83,7 @@ if (isset($_GET['purged'])) $notice = '<div class="notice notice-success"
</tr> </tr>
<tr> <tr>
<th>Только за последние</th> <th>Только за последние</th>
<td><input type="number" name="retro_days" value="7" min="0"> дней <span class="description">(0 = без ограничения)</span></td> <td><input type="number" name="retro_days" value="0" min="0"> дней <span class="description">(0 = без ограничения)</span></td>
</tr> </tr>
<tr> <tr>
<th>&nbsp;</th> <th>&nbsp;</th>
@@ -2482,7 +2482,7 @@ final class Settings_Page
{ {
$this->guard('cf7stg_retro'); $this->guard('cf7stg_retro');
$count = max(1, min(RetroImporter::MAX_COUNT, (int)($_POST['retro_count'] ?? 10))); $count = max(1, min(RetroImporter::MAX_COUNT, (int)($_POST['retro_count'] ?? 10)));
$days = max(0, (int)($_POST['retro_days'] ?? 7)); $days = max(0, (int)($_POST['retro_days'] ?? 0));
$repeat = !empty($_POST['retro_repeat']); $repeat = !empty($_POST['retro_repeat']);
$result = RetroImporter::import($count, $days, $repeat); $result = RetroImporter::import($count, $days, $repeat);
$this->redirect_back(['retro_added' => (int)$result['added'], 'retro_skipped' => (int)$result['skipped']]); $this->redirect_back(['retro_added' => (int)$result['added'], 'retro_skipped' => (int)$result['skipped']]);
@@ -2605,7 +2605,7 @@ if (isset($_GET['purged'])) $notice = '<div class="notice notice-success"
</tr> </tr>
<tr> <tr>
<th>Только за последние</th> <th>Только за последние</th>
<td><input type="number" name="retro_days" value="7" min="0"> дней <span class="description">(0 = без ограничения)</span></td> <td><input type="number" name="retro_days" value="0" min="0"> дней <span class="description">(0 = без ограничения)</span></td>
</tr> </tr>
<tr> <tr>
<th>&nbsp;</th> <th>&nbsp;</th>
@@ -2660,15 +2660,18 @@ final class Dashboard_Widget
{ {
public function register(): void public function register(): void
{ {
add_action('wp_dashboard_setup', [$this, 'maybe_register_widget']); add_action('wp_dashboard_setup', [$this, 'register_widget']);
} }
public function maybe_register_widget(): void public function register_widget(): void
{ {
if (Queue::count_failed() === 0) return; $failed_count = Queue::count_failed();
$title = $failed_count > 0
? '⚠️ CF7 Spam → Telegram: ошибки доставки'
: 'CF7 Spam → Telegram';
wp_add_dashboard_widget( wp_add_dashboard_widget(
'cf7stg_dashboard_errors', 'cf7stg_dashboard_status',
'⚠️ CF7 Spam → Telegram: ошибки доставки', $title,
[$this, 'render'] [$this, 'render']
); );
} }
@@ -2676,7 +2679,10 @@ final class Dashboard_Widget
public function render(): void public function render(): void
{ {
$failed = Queue::list_failed(10); $failed = Queue::list_failed(10);
$count = Queue::count_failed(); $failed_count = Queue::count_failed();
$pending = Queue::count_by_status(Queue::STATUS_PENDING);
$sent_24h = Queue::count_sent_last_24h();
$configured = Settings::is_configured();
$settings_url = admin_url('options-general.php?page=cf7stg-settings'); $settings_url = admin_url('options-general.php?page=cf7stg-settings');
include CF7STG_PLUGIN_DIR . 'admin/views/dashboard-widget.php'; include CF7STG_PLUGIN_DIR . 'admin/views/dashboard-widget.php';
} }
@@ -2688,10 +2694,28 @@ final class Dashboard_Widget
```php ```php
<?php <?php
/** @var array<int,object> $failed */ /** @var array<int,object> $failed */
/** @var int $count */ /** @var int $failed_count */
/** @var int $pending */
/** @var int $sent_24h */
/** @var bool $configured */
/** @var string $settings_url */ /** @var string $settings_url */
?> ?>
<p>Есть <b><?php echo (int)$count; ?></b> неотправленных сообщений (TG-API отвечает ошибкой):</p> <?php if (!$configured): ?>
<p>⚙️ Плагин не настроен — укажите <b>Bot Token</b> и <b>Chat ID</b> в настройках.</p>
<?php endif; ?>
<p>
<span title="Ожидают отправки">⏳ В очереди: <b><?php echo (int)$pending; ?></b></span>
&nbsp;·&nbsp;
<span title="Успешно отправлено за последние сутки">✅ За сутки: <b><?php echo (int)$sent_24h; ?></b></span>
&nbsp;·&nbsp;
<span title="Не удалось доставить после ретраев">
<?php echo $failed_count > 0 ? '⚠️' : '✖'; ?> Ошибки: <b><?php echo (int)$failed_count; ?></b>
</span>
</p>
<?php if ($failed_count > 0): ?>
<p style="margin-top:10px">Последние ошибки доставки (TG-API):</p>
<ul style="margin:0 0 8px 18px;list-style:disc"> <ul style="margin:0 0 8px 18px;list-style:disc">
<?php foreach ($failed as $row): ?> <?php foreach ($failed as $row): ?>
<li> <li>
@@ -2700,6 +2724,8 @@ final class Dashboard_Widget
</li> </li>
<?php endforeach; ?> <?php endforeach; ?>
</ul> </ul>
<?php endif; ?>
<p><a href="<?php echo esc_url($settings_url); ?>" class="button">Открыть настройки →</a></p> <p><a href="<?php echo esc_url($settings_url); ?>" class="button">Открыть настройки →</a></p>
``` ```
@@ -257,7 +257,7 @@ apply_filters('cf7stg_classifier_thresholds', ['client' => 3, 'spam' => -2]);
**UI:** на странице настроек — блок «Ретроспективная выгрузка из Flamingo»: **UI:** на странице настроек — блок «Ретроспективная выгрузка из Flamingo»:
- `count` (int, default 10, max 50) - `count` (int, default 10, max 50)
- `days` (int, default 7; 0 = без ограничения) - `days` (int, default 0; 0 = без ограничения)
- `allow_repeat` (checkbox) - `allow_repeat` (checkbox)
- кнопка «Добавить в очередь» - кнопка «Добавить в очередь»
@@ -324,22 +324,30 @@ $schedules['cf7stg_every_minute'] = ['interval' => 60, 'display' => 'Раз в
**Ограничения shared-хостинга:** WP-Cron срабатывает при HTTP-запросах к сайту. При малом трафике события могут опаздывать. В UI настроек — короткая подсказка: «Для точности можно настроить внешний cron на `wp-cron.php`». **Ограничения shared-хостинга:** WP-Cron срабатывает при HTTP-запросах к сайту. При малом трафике события могут опаздывать. В UI настроек — короткая подсказка: «Для точности можно настроить внешний cron на `wp-cron.php`».
## 14. Dashboard-виджет (условный) ## 14. Dashboard-виджет
`add_action('wp_dashboard_setup', ...)` → при `Queue::count_failed() > 0` регистрируется виджет `cf7stg_dashboard_errors`: `add_action('wp_dashboard_setup', ...)` регистрирует виджет `cf7stg_dashboard_status` **всегда** (после пилота: пользователь ожидает увидеть быстрый статус, а не ориентироваться только на появление «тревожной» лампы).
Заголовок зависит от состояния:
- `failed > 0``⚠️ CF7 Spam → Telegram: ошибки доставки`
- иначе → `CF7 Spam → Telegram`
Содержимое:
``` ```
⚠️ CF7 Spam → Telegram: ошибки доставки [если !is_configured: ⚙️ Плагин не настроен — укажите Bot Token и Chat ID в настройках.]
Есть N неотправленных сообщений (TG-API отвечает ошибкой):
⏳ В очереди: N · ✅ За сутки: M · [✖|⚠️] Ошибки: K
[если K > 0:
Последние ошибки доставки (TG-API):
• {created_at} — {last_error} • {created_at} — {last_error}
• ... • ...
]
[Открыть настройки →] [Открыть настройки →]
``` ```
При `count_failed() == 0` — виджет не регистрируется, дашборд чистый.
## 15. Обработка ошибок Telegram API ## 15. Обработка ошибок Telegram API
`TelegramClient::send_message()` бросает `TelegramException`, у которого метод `is_permanent(): bool`. `TelegramClient::send_message()` бросает `TelegramException`, у которого метод `is_permanent(): bool`.