| @@ -60,9 +60,9 @@ | ||
| 60 | 60 | |
| 61 | 61 | /** |
| 62 | 62 | * The resolved config every consumer reads. |
| 63 | 63 | * |
| 64 | - * @return array{enabled: bool, frequency_days: int, recipients: string[], sections_enabled: string[], next_scheduled_at: ?string, last_sent_at: ?string} | |
| 64 | + * @return array{enabled: bool, frequency_days: int, recipients: string[], sections_enabled: string[], next_scheduled_at: ?string, last_sent_at: ?string, last_skip: ?array} | |
| 65 | 65 | */ |
| 66 | 66 | public function get(): array { |
| 67 | 67 | $this->load_defaults_file(); |
| 68 | 68 | |
| @@ -70,8 +70,11 @@ | ||
| 70 | 70 | $state = [ |
| 71 | 71 | 'enabled' => !empty($stored['enabled']), |
| 72 | 72 | 'next_scheduled_at' => $stored['next_scheduled_at'] ?? null, |
| 73 | 73 | 'last_sent_at' => $stored['last_sent_at'] ?? null, |
| 74 | + // Why the last scheduled run sent nothing, or null. Read by the | |
| 75 | + // panel so a paused report is never mistaken for a healthy one. | |
| 76 | + 'last_skip' => is_array($stored['last_skip'] ?? null) ? $stored['last_skip'] : null, | |
| 74 | 77 | ]; |
| 75 | 78 | |
| 76 | 79 | $report = [ |
| 77 | 80 | 'frequency_days' => self::FREQUENCY_DAYS, |
| @@ -182,8 +185,47 @@ | ||
| 182 | 185 | return $this->update_schedule( |
| 183 | 186 | $config['last_sent_at'], |
| 184 | 187 | wp_date('Y-m-d H:i:s', max($next ?: time(), time())) |
| 185 | 188 | ); |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * Note why a scheduled run sent nothing (#742). | |
| 193 | + * | |
| 194 | + * `search_console_not_connected` leaves the schedule alone so the next | |
| 195 | + * hourly tick tries again; `no_data` is recorded by the generator after | |
| 196 | + * it has already pushed the schedule out a period. Either way the panel | |
| 197 | + * shows the reason and when it was last seen. | |
| 198 | + * | |
| 199 | + * `fetch_failed` (#912) is recorded by the generator when Search Console | |
| 200 | + * answered with an error; it carries the API's message so the panel can | |
| 201 | + * say what went wrong, and the schedule was moved to a retry. | |
| 202 | + * | |
| 203 | + * @param string $reason Machine-readable reason. | |
| 204 | + * @param string $message Optional plain-text detail, e.g. the API error. | |
| 205 | + */ | |
| 206 | + public function record_skip(string $reason, string $message = ''): void { | |
| 207 | + $stored = $this->stored(); | |
| 208 | + $stored['last_skip'] = [ | |
| 209 | + 'reason' => sanitize_key($reason), | |
| 210 | + 'at' => current_time('mysql'), | |
| 211 | + ]; | |
| 212 | + if ($message !== '') { | |
| 213 | + $stored['last_skip']['message'] = sanitize_text_field($message); | |
| 214 | + } | |
| 215 | + update_option(self::OPTION_KEY, $stored, false); | |
| 216 | + } | |
| 217 | + | |
| 218 | + /** | |
| 219 | + * A report went out — whatever paused it earlier no longer applies. | |
| 220 | + */ | |
| 221 | + public function clear_skip(): void { | |
| 222 | + $stored = $this->stored(); | |
| 223 | + if (!array_key_exists('last_skip', $stored)) { | |
| 224 | + return; | |
| 225 | + } | |
| 226 | + unset($stored['last_skip']); | |
| 227 | + update_option(self::OPTION_KEY, $stored, false); | |
| 186 | 228 | } |
| 187 | 229 | |
| 188 | 230 | /** |
| 189 | 231 | * Update only the schedule timestamps. Called from the scheduler after |