'boolean', 'present' => 'boolean', 'last_handshake_at' => 'datetime', 'secret_purged_at' => 'datetime', 'last_downloaded_at' => 'datetime', 'observed_at' => 'datetime', 'rx_bytes' => 'integer', 'tx_bytes' => 'integer', ]; } public function uniqueIds(): array { return ['uuid']; } public function host(): BelongsTo { return $this->belongsTo(Host::class); } public function creator(): BelongsTo { return $this->belongsTo(Operator::class, 'created_by'); } /** The person this access belongs to (staff peers only). */ public function owner(): BelongsTo { return $this->belongsTo(Operator::class, 'user_id'); } public function hasStoredConfig(): bool { return $this->config_secret !== null; } /** * Overwrite the stored credential. Called on revocation: a tombstone that * still carries a usable private key is a liability, not a record. */ public function purgeSecret(): void { if ($this->config_secret === null) { return; } $this->forceFill(['config_secret' => null, 'secret_purged_at' => now()])->saveQuietly(); } public function isOnline(): bool { return $this->enabled && $this->present && $this->last_handshake_at !== null && $this->last_handshake_at->gt(now()->subMinutes(self::ONLINE_AFTER_MINUTES)); } /** * blocked — operator revoked it; it must not be on the hub * pending — desired and observed state disagree; a job is still applying it * online — configured and handshaking * idle — configured, but no recent handshake */ public function status(): string { if (! $this->enabled) { return $this->present ? 'pending' : 'blocked'; } if (! $this->present) { return 'pending'; } return $this->isOnline() ? 'online' : 'idle'; } }