Strengthen push device health checks
This commit is contained in:
+201
-2
@@ -262,9 +262,18 @@ function bootstrap_db(): void
|
||||
device_name VARCHAR(64) NULL,
|
||||
user_agent VARCHAR(255) NULL,
|
||||
actor_ip VARCHAR(64) NULL,
|
||||
last_send_success_at DATETIME NULL,
|
||||
last_send_failed_at DATETIME NULL,
|
||||
last_received_at DATETIME NULL,
|
||||
last_notification_at DATETIME NULL,
|
||||
last_click_at DATETIME NULL,
|
||||
failure_count INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
last_failure_reason VARCHAR(255) NULL,
|
||||
|
||||
UNIQUE KEY uniq_endpoint_hash (endpoint_hash),
|
||||
INDEX idx_last_seen_at (last_seen_at)
|
||||
INDEX idx_last_seen_at (last_seen_at),
|
||||
INDEX idx_last_received_at (last_received_at),
|
||||
INDEX idx_last_send_success_at (last_send_success_at)
|
||||
) ENGINE=InnoDB
|
||||
DEFAULT CHARSET=utf8mb4
|
||||
COLLATE=utf8mb4_unicode_ci
|
||||
@@ -354,6 +363,15 @@ function bootstrap_db(): void
|
||||
"ALTER TABLE system_notice_state ADD COLUMN active_rpm_delta DECIMAL(12,2) NULL AFTER active_temp_delta",
|
||||
"ALTER TABLE system_notice_state ADD COLUMN process_signature VARCHAR(255) NULL AFTER active_rpm_delta",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN device_name VARCHAR(64) NULL AFTER content_encoding",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_send_success_at DATETIME NULL AFTER actor_ip",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_send_failed_at DATETIME NULL AFTER last_send_success_at",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_received_at DATETIME NULL AFTER last_send_failed_at",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_notification_at DATETIME NULL AFTER last_received_at",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_click_at DATETIME NULL AFTER last_notification_at",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN failure_count INT UNSIGNED NOT NULL DEFAULT 0 AFTER last_click_at",
|
||||
"ALTER TABLE push_subscriptions ADD COLUMN last_failure_reason VARCHAR(255) NULL AFTER failure_count",
|
||||
"ALTER TABLE push_subscriptions ADD INDEX idx_last_received_at (last_received_at)",
|
||||
"ALTER TABLE push_subscriptions ADD INDEX idx_last_send_success_at (last_send_success_at)",
|
||||
"ALTER TABLE sensor_logs DROP COLUMN disk_total_gb",
|
||||
"ALTER TABLE sensor_logs DROP COLUMN disk_used_gb",
|
||||
"ALTER TABLE sensor_logs DROP COLUMN disk_free_gb",
|
||||
@@ -499,6 +517,76 @@ function push_log_event(string $event, array $context = []): void
|
||||
':message' => isset($context['message']) ? mb_substr((string)$context['message'], 0, 255) : null,
|
||||
':meta' => json_encode($context, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
]);
|
||||
|
||||
update_push_subscription_health($event, $endpointHash, $context);
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
|
||||
function update_push_subscription_health(string $event, string $endpointHash, array $context = []): void
|
||||
{
|
||||
if (!preg_match('/^[a-f0-9]{64}$/', $endpointHash)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$reason = mb_substr((string)($context['reason'] ?? ''), 0, 255);
|
||||
|
||||
try {
|
||||
if (in_array($event, ['register', 'register_update'], true)) {
|
||||
$stmt = db()->prepare("
|
||||
UPDATE push_subscriptions
|
||||
SET last_seen_at = CURRENT_TIMESTAMP,
|
||||
failure_count = 0,
|
||||
last_failure_reason = NULL
|
||||
WHERE endpoint_hash = :endpoint_hash
|
||||
");
|
||||
$stmt->execute([':endpoint_hash' => $endpointHash]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event === 'send_success') {
|
||||
$stmt = db()->prepare("
|
||||
UPDATE push_subscriptions
|
||||
SET last_send_success_at = CURRENT_TIMESTAMP,
|
||||
failure_count = 0,
|
||||
last_failure_reason = NULL
|
||||
WHERE endpoint_hash = :endpoint_hash
|
||||
");
|
||||
$stmt->execute([':endpoint_hash' => $endpointHash]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event === 'send_failed') {
|
||||
$stmt = db()->prepare("
|
||||
UPDATE push_subscriptions
|
||||
SET last_send_failed_at = CURRENT_TIMESTAMP,
|
||||
failure_count = failure_count + 1,
|
||||
last_failure_reason = :reason
|
||||
WHERE endpoint_hash = :endpoint_hash
|
||||
");
|
||||
$stmt->execute([
|
||||
':endpoint_hash' => $endpointHash,
|
||||
':reason' => $reason !== '' ? $reason : 'send_failed',
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$columnByEvent = [
|
||||
'push_received' => 'last_received_at',
|
||||
'notification_shown' => 'last_notification_at',
|
||||
'notification_click' => 'last_click_at',
|
||||
];
|
||||
if (isset($columnByEvent[$event])) {
|
||||
$column = $columnByEvent[$event];
|
||||
$stmt = db()->prepare("
|
||||
UPDATE push_subscriptions
|
||||
SET {$column} = CURRENT_TIMESTAMP,
|
||||
failure_count = 0,
|
||||
last_failure_reason = NULL
|
||||
WHERE endpoint_hash = :endpoint_hash
|
||||
");
|
||||
$stmt->execute([':endpoint_hash' => $endpointHash]);
|
||||
}
|
||||
} catch (Throwable) {
|
||||
}
|
||||
}
|
||||
@@ -597,7 +685,18 @@ function push_device_rows(): array
|
||||
user_agent,
|
||||
actor_ip,
|
||||
created_at,
|
||||
last_seen_at
|
||||
last_seen_at,
|
||||
last_send_success_at,
|
||||
last_send_failed_at,
|
||||
last_received_at,
|
||||
last_notification_at,
|
||||
last_click_at,
|
||||
failure_count,
|
||||
last_failure_reason,
|
||||
TIMESTAMPDIFF(SECOND, last_seen_at, NOW()) AS last_seen_seconds,
|
||||
TIMESTAMPDIFF(SECOND, last_send_success_at, NOW()) AS last_send_success_seconds,
|
||||
TIMESTAMPDIFF(SECOND, last_received_at, NOW()) AS last_received_seconds,
|
||||
TIMESTAMPDIFF(SECOND, last_notification_at, NOW()) AS last_notification_seconds
|
||||
FROM push_subscriptions
|
||||
ORDER BY last_seen_at DESC
|
||||
LIMIT 200
|
||||
@@ -606,6 +705,26 @@ function push_device_rows(): array
|
||||
$rows = [];
|
||||
foreach ($stmt->fetchAll() as $row) {
|
||||
$endpoint = (string)($row['endpoint'] ?? '');
|
||||
$failureCount = (int)($row['failure_count'] ?? 0);
|
||||
$lastReceivedSeconds = isset($row['last_received_seconds']) ? (int)$row['last_received_seconds'] : null;
|
||||
$lastSendSuccessSeconds = isset($row['last_send_success_seconds']) ? (int)$row['last_send_success_seconds'] : null;
|
||||
$status = 'pending';
|
||||
$statusText = '수신 대기';
|
||||
|
||||
if ($failureCount >= 3) {
|
||||
$status = 'failed';
|
||||
$statusText = '발송 실패 누적';
|
||||
} elseif ($lastReceivedSeconds !== null && $lastReceivedSeconds <= 86400) {
|
||||
$status = 'healthy';
|
||||
$statusText = '정상';
|
||||
} elseif ($lastReceivedSeconds !== null && $lastReceivedSeconds <= 604800) {
|
||||
$status = 'watch';
|
||||
$statusText = '수신 지연';
|
||||
} elseif ($lastSendSuccessSeconds !== null) {
|
||||
$status = 'stale';
|
||||
$statusText = '장기 미수신';
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'hash' => (string)($row['endpoint_hash'] ?? hash('sha256', $endpoint)),
|
||||
'endpoint' => $endpoint,
|
||||
@@ -616,12 +735,48 @@ function push_device_rows(): array
|
||||
'actor_ip' => (string)($row['actor_ip'] ?? ''),
|
||||
'created_at' => (string)($row['created_at'] ?? ''),
|
||||
'last_seen_at' => (string)($row['last_seen_at'] ?? ''),
|
||||
'last_send_success_at' => (string)($row['last_send_success_at'] ?? ''),
|
||||
'last_send_failed_at' => (string)($row['last_send_failed_at'] ?? ''),
|
||||
'last_received_at' => (string)($row['last_received_at'] ?? ''),
|
||||
'last_notification_at' => (string)($row['last_notification_at'] ?? ''),
|
||||
'last_click_at' => (string)($row['last_click_at'] ?? ''),
|
||||
'last_seen_seconds' => $row['last_seen_seconds'] !== null ? (int)$row['last_seen_seconds'] : null,
|
||||
'last_send_success_seconds' => $lastSendSuccessSeconds,
|
||||
'last_received_seconds' => $lastReceivedSeconds,
|
||||
'last_notification_seconds' => $row['last_notification_seconds'] !== null ? (int)$row['last_notification_seconds'] : null,
|
||||
'failure_count' => $failureCount,
|
||||
'last_failure_reason' => (string)($row['last_failure_reason'] ?? ''),
|
||||
'health_status' => $status,
|
||||
'health_text' => $statusText,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
function push_health_summary(): array
|
||||
{
|
||||
$devices = push_device_rows();
|
||||
$summary = [
|
||||
'total' => count($devices),
|
||||
'healthy' => 0,
|
||||
'watch' => 0,
|
||||
'stale' => 0,
|
||||
'failed' => 0,
|
||||
'pending' => 0,
|
||||
];
|
||||
|
||||
foreach ($devices as $device) {
|
||||
$status = (string)($device['health_status'] ?? 'pending');
|
||||
if (!array_key_exists($status, $summary)) {
|
||||
$status = 'pending';
|
||||
}
|
||||
$summary[$status]++;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
function push_subscription_status(string $endpoint): array
|
||||
{
|
||||
$endpointHash = $endpoint !== '' ? hash('sha256', $endpoint) : '';
|
||||
@@ -808,6 +963,50 @@ function send_push_payload(array $payload): array
|
||||
];
|
||||
}
|
||||
|
||||
function latest_push_send_epoch_by_tag(string $tagPrefix): int
|
||||
{
|
||||
$stmt = db()->prepare("
|
||||
SELECT UNIX_TIMESTAMP(MAX(created_at))
|
||||
FROM push_event_logs
|
||||
WHERE event = 'send_success'
|
||||
AND meta LIKE :tag
|
||||
");
|
||||
$stmt->execute([
|
||||
':tag' => '%\"tag\":\"' . str_replace(['%', '_'], ['\\%', '\\_'], $tagPrefix) . '%',
|
||||
]);
|
||||
|
||||
return (int)($stmt->fetchColumn() ?: 0);
|
||||
}
|
||||
|
||||
function send_push_healthcheck_if_due(int $minHours = 24, bool $force = false): array
|
||||
{
|
||||
$minHours = max(1, min(168, $minHours));
|
||||
$latest = latest_push_send_epoch_by_tag('control-healthcheck-');
|
||||
|
||||
if (!$force && $latest > 0 && time() - $latest < $minHours * 3600) {
|
||||
return [
|
||||
'sent' => 0,
|
||||
'failed' => 0,
|
||||
'error' => null,
|
||||
'skipped' => true,
|
||||
'message' => 'healthcheck_cooldown',
|
||||
'next_after_seconds' => ($minHours * 3600) - (time() - $latest),
|
||||
];
|
||||
}
|
||||
|
||||
return send_push_payload([
|
||||
'title' => 'Seoul Control Center',
|
||||
'body' => '푸시 기기 상태 확인 알림입니다.',
|
||||
'url' => '/',
|
||||
'tag' => 'control-healthcheck-' . date('YmdHi'),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'silent' => true,
|
||||
'data' => [
|
||||
'kind' => 'push_healthcheck',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
function reset_battery_low_push_state(): void
|
||||
{
|
||||
$stmt = db()->prepare("
|
||||
|
||||
Reference in New Issue
Block a user