{ "schema_version": 1, "channel": "homelab-runtime", "command_id": "SUPPORT-260918-GRAM-SUPPORT-STAFF-EMAIL-SOURCE-READONLY-516R1R4", "status": "OK", "rc": 0, "host": "pve01", "mode": "read-only", "component": "gram-support-staff-email-source-readonly", "started_at_utc": "2026-09-18T05:14:57Z", "finished_at_utc": "2026-09-18T05:15:08Z", "reference_register_checked": true, "reference_sha256": "5a3d8e5154c41cb582a4e0aca68090be1f0138918bf82131a948df326f9d8d66", "error_register_checked": true, "error_register_sha256": "3b09a553ec0f527ed3afeed4753f52a74ee3036045bbfb3c685e9f8af4ba7ba0", "command_sha256": "08ff6acd78dea5c4a5621dcca2fedad234c67817f25ff1f3b4ee9b9b2d14c5f0", "duplicate_failed_command_blocked": false, "block_reason": null, "execution_started": true, "change_declared": false, "result_contract_valid": true, "result_contract_status": null, "result_contract_error": null, "command_rc": 0, "changes_made": false, "rollback_started": false, "rollback_restored": null, "mutation_outcome": "NO_MUTATION", "sanitized": true, "secrets_included": false, "private_addresses_included": false, "raw_evidence_retained_locally": true, "raw_evidence_sha256": "3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a", "sanitized_output_sha256": "3375e95bfa30e7b6adf9c22d4a03b7407be9113f1d2ed450ec96bed61add1e9a", "output_truncated_in_json": false, "full_sanitized_output_url": "https://git.gram1.ru/.well-known/homelab-runtime/latest.txt", "output": "GRAM516R1R4_SAFE_REPORT={\"classification\":{\"all_sites_eligible_staff_match_target\":true,\"changes_made\":false,\"eligible_staff_target_match_total\":7,\"eligible_staff_total\":7,\"email_consumer_present\":true,\"event_dispatcher_present\":true,\"runtime_probe_7of7\":true,\"send_attempts\":0,\"source_probe_ok\":true},\"schema_version\":1,\"scope\":\"READ_ONLY_GRAM_SUPPORT_STAFF_EMAIL_SOURCE\",\"secret_policy\":\"NO_NON_TARGET_EMAIL_ADDRESSES_EXPORTED\",\"sites\":[{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"codevipe\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"dsmods\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"gamevipe\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"hkmods\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"kingofwolk\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"zakrutim\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]},{\"all_eligible_match_target\":true,\"eligible_staff_count\":1,\"eligible_target_match_count\":1,\"slug\":\"newfi\",\"staff\":[{\"email_matches_target\":true,\"email_nonempty\":true,\"gram_manage\":true,\"gram_viewAll\":true,\"is_admin\":true,\"is_moderator\":true,\"primary_group_id\":2,\"secondary_group_ids\":[3,4],\"staff_eligible\":true,\"user_id_sha256\":\"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b\"}]}],\"source\":{\"files\":{\"Notification/DeliveryGate.php\":{\"line_count\":16,\"present\":true,\"text\":\"alerts = $alerts;\\n $this->email = $email;\\n }\\n\\n /**\\n * Owner-facing delivery. Staff alert fan-out is intentionally provided by\\n * dispatchStaff() so staff recipients must still pass Gram Support ACL.\\n */\\n public function dispatchOwner(SupportEvent $event, Ticket $ticket, ?User $owner): void\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return;\\n }\\n\\n if ($owner && $owner->user_id)\\n {\\n $this->alerts->consumeForUser($event, $ticket, $owner);\\n $this->email->consumeForRegistered($event, $ticket, $owner);\\n return;\\n }\\n\\n $this->email->consumeForGuest($event, $ticket);\\n }\\n\\n /**\\n * @param User[] $staffUsers\\n */\\n public function dispatchStaff(SupportEvent $event, Ticket $ticket, array $staffUsers): void\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return;\\n }\\n\\n foreach ($staffUsers as $staff)\\n {\\n if (!$staff instanceof User || !$staff->user_id)\\n {\\n continue;\\n }\\n\\n if (!$staff->hasPermission('gramSupport', 'viewAll')\\n && !$staff->hasPermission('gramSupport', 'manage'))\\n {\\n continue;\\n }\\n\\n $this->alerts->consumeForUser($event, $ticket, $staff);\\n }\\n }\\n}\\n\"},\"Notification/EmailConsumer.php\":{\"line_count\":107,\"present\":true,\"text\":\"gate = $gate;\\n }\\n\\n public function consumeForRegistered(SupportEvent $event, Ticket $ticket, User $recipient): bool\\n {\\n if (!Policy::mayEmitExternal($event) || !$recipient->user_id)\\n {\\n return false;\\n }\\n\\n if (!$ticket->user_id || (int)$ticket->user_id !== (int)$recipient->user_id)\\n {\\n return false;\\n }\\n\\n $recipientKey = RecipientKey::user((int)$recipient->user_id);\\n return $this->send(\\n $event,\\n $recipientKey,\\n function () use ($event, $ticket, $recipient)\\n {\\n \\\\XF::app()->mailer()->newMail()\\n ->setToUser($recipient)\\n ->setTemplate('gram_support_ticket_event', [\\n 'ticket' => $ticket,\\n 'event' => $event,\\n 'accessUrl' => null\\n ])\\n ->queue();\\n }\\n );\\n }\\n\\n public function consumeForGuest(SupportEvent $event, Ticket $ticket): bool\\n {\\n if (!Policy::mayEmitExternal($event))\\n {\\n return false;\\n }\\n\\n $email = trim((string)$ticket->guest_email);\\n if ($email === '')\\n {\\n return false;\\n }\\n\\n $recipientKey = RecipientKey::email($email);\\n\\n return $this->send(\\n $event,\\n $recipientKey,\\n function () use ($event, $ticket, $email)\\n {\\n // Raw guest access token is intentionally unavailable here:\\n // only its hash is persisted. Email links to the manual access\\n // page; the guest uses the one-time/saved access code.\\n \\\\XF::app()->mailer()->newMail()\\n ->setTo($email)\\n ->setTemplate('gram_support_ticket_event_guest', [\\n 'ticket' => $ticket,\\n 'event' => $event,\\n 'accessUrl' => \\\\XF::app()->router('public')->buildLink('canonical:support/access')\\n ])\\n ->queue();\\n }\\n );\\n }\\n\\n private function send(SupportEvent $event, string $recipientKey, callable $sender): bool\\n {\\n if (!$this->gate->acquire((int)$event->event_id, 'email', $recipientKey))\\n {\\n return false;\\n }\\n\\n try\\n {\\n $sender();\\n $this->gate->markSent((int)$event->event_id, 'email', $recipientKey);\\n return true;\\n }\\n catch (\\\\Throwable $e)\\n {\\n $this->gate->markFailed(\\n (int)$event->event_id,\\n 'email',\\n $recipientKey,\\n get_class($e)\\n );\\n throw $e;\\n }\\n }\\n}\\n\"},\"Notification/EntityDeliveryGate.php\":{\"line_count\":118,\"present\":true,\"text\":\"deliveryKey($eventId, $consumer, $recipientKey);\\n $lockName = 'gram_support_delivery_' . substr($deliveryKey, 0, 32);\\n $db = \\\\XF::db();\\n\\n if ((int)$db->fetchOne('SELECT GET_LOCK(?, 2)', [$lockName]) !== 1)\\n {\\n return false;\\n }\\n\\n try\\n {\\n $now = \\\\XF::$time;\\n $delivery = \\\\XF::em()->find('Gram\\\\Support:Delivery', $deliveryKey);\\n\\n if (!$delivery)\\n {\\n $delivery = \\\\XF::em()->create('Gram\\\\Support:Delivery');\\n $delivery->delivery_key = $deliveryKey;\\n $delivery->event_id = $eventId;\\n $delivery->consumer = $consumer;\\n $delivery->recipient_key = $recipientKey;\\n $delivery->state = 'pending';\\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\\n $delivery->attempt_count = 1;\\n $delivery->created_date = $now;\\n $delivery->updated_date = $now;\\n $delivery->save();\\n return true;\\n }\\n\\n if ((string)$delivery->state === 'sent')\\n {\\n return false;\\n }\\n\\n if ((string)$delivery->state === 'pending' && (int)$delivery->lease_expires > $now)\\n {\\n return false;\\n }\\n\\n $delivery->state = 'pending';\\n $delivery->lease_expires = $now + self::LEASE_SECONDS;\\n $delivery->attempt_count = (int)$delivery->attempt_count + 1;\\n $delivery->updated_date = $now;\\n $delivery->save();\\n\\n return true;\\n }\\n finally\\n {\\n try\\n {\\n $db->fetchOne('SELECT RELEASE_LOCK(?)', [$lockName]);\\n }\\n catch (\\\\Throwable $ignored)\\n {\\n }\\n }\\n }\\n\\n public function markSent(int $eventId, string $consumer, string $recipientKey): void\\n {\\n $delivery = \\\\XF::em()->find(\\n 'Gram\\\\Support:Delivery',\\n $this->deliveryKey($eventId, $consumer, $recipientKey)\\n );\\n\\n if (!$delivery)\\n {\\n return;\\n }\\n\\n $delivery->state = 'sent';\\n $delivery->lease_expires = 0;\\n $delivery->updated_date = \\\\XF::$time;\\n $delivery->sent_date = \\\\XF::$time;\\n $delivery->save();\\n }\\n\\n public function markFailed(int $eventId, string $consumer, string $recipientKey, string $failureClass): void\\n {\\n $delivery = \\\\XF::em()->find(\\n 'Gram\\\\Support:Delivery',\\n $this->deliveryKey($eventId, $consumer, $recipientKey)\\n );\\n\\n if (!$delivery)\\n {\\n return;\\n }\\n\\n $delivery->state = 'failed';\\n $delivery->lease_expires = 0;\\n $delivery->last_failure_class = substr($failureClass, 0, 191);\\n $delivery->updated_date = \\\\XF::$time;\\n $delivery->save();\\n }\\n\\n private function deliveryKey(int $eventId, string $consumer, string $recipientKey): string\\n {\\n return hash('sha256', $eventId . \\\"\\\\0\\\" . $consumer . \\\"\\\\0\\\" . $recipientKey);\\n }\\n}\\n\"},\"Notification/EventDispatcher.php\":{\"line_count\":42,\"present\":true,\"text\":\"user_id > 0)\\n {\\n $owner = \\\\XF::em()->find('XF:User', (int)$ticket->user_id);\\n }\\n\\n $dispatcher->dispatchOwner($event, $ticket, $owner);\\n\\n if ((int)$ticket->assigned_user_id > 0)\\n {\\n $staff = \\\\XF::em()->find('XF:User', (int)$ticket->assigned_user_id);\\n if ($staff)\\n {\\n $dispatcher->dispatchStaff($event, $ticket, [$staff]);\\n }\\n }\\n }\\n catch (\\\\Throwable $e)\\n {\\n // Event/Ticket/Message are already persisted truth.\\n \\\\XF::logException($e, false, 'Gram Support notification consumer: ');\\n }\\n }\\n}\\n\"},\"Notification/Policy.php\":{\"line_count\":61,\"present\":true,\"text\":\"event_type, self::INTERNAL_EVENT_TYPES, true))\\n {\\n return false;\\n }\\n\\n $payload = self::safePayload($event);\\n\\n if (!empty($payload['is_internal']))\\n {\\n return false;\\n }\\n\\n return true;\\n }\\n\\n public static function safePayload(SupportEvent $event): array\\n {\\n $payload = $event->safe_payload_json;\\n\\n if (is_array($payload))\\n {\\n return $payload;\\n }\\n\\n if (!is_string($payload) || $payload === '')\\n {\\n return [];\\n }\\n\\n $decoded = json_decode($payload, true);\\n return is_array($decoded) ? $decoded : [];\\n }\\n\\n public static function alertExtraData(SupportEvent $event): array\\n {\\n $payload = self::safePayload($event);\\n\\n return [\\n 'support_event_id' => (int)$event->event_id,\\n 'ticket_id' => (int)$event->ticket_id,\\n 'event_type' => (string)$event->event_type,\\n 'public_ref' => isset($payload['public_ref']) ? (string)$payload['public_ref'] : ''\\n ];\\n }\\n}\\n\"},\"Notification/RecipientKey.php\":{\"line_count\":28,\"present\":true,\"text\":\"