fix width

This commit is contained in:
Purple
2026-01-17 23:34:40 +00:00
parent a2d25266fb
commit 17fd69c4f7
3 changed files with 468 additions and 57 deletions

View File

@@ -116,6 +116,27 @@ CREATE TABLE IF NOT EXISTS user_sessions (
INDEX idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- PTR records cache table for Route53 A records
CREATE TABLE IF NOT EXISTS ptr_records_cache (
id INT AUTO_INCREMENT PRIMARY KEY,
zone_id VARCHAR(50) NOT NULL,
zone_name VARCHAR(255) DEFAULT NULL,
hostname VARCHAR(255) NOT NULL,
ip_address VARCHAR(45) NOT NULL,
ttl INT DEFAULT NULL,
ptr_record VARCHAR(255) DEFAULT NULL,
ptr_status ENUM('unknown', 'match', 'mismatch', 'missing', 'error') DEFAULT 'unknown',
ptr_checked_at TIMESTAMP NULL DEFAULT NULL,
aws_synced_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY unique_zone_hostname (zone_id, hostname),
INDEX idx_zone (zone_id),
INDEX idx_ip (ip_address),
INDEX idx_status (ptr_status),
INDEX idx_synced (aws_synced_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Insert default settings
INSERT INTO geofeed_settings (setting_key, setting_value) VALUES
('bunny_cdn_storage_zone', ''),

View File

@@ -194,6 +194,22 @@ try {
handlePtrLookup($db);
break;
case 'ptr_cache_get':
handlePtrCacheGet($db);
break;
case 'ptr_cache_refresh':
handlePtrCacheRefresh($db);
break;
case 'ptr_check_all':
handlePtrCheckAll($db);
break;
case 'ptr_cache_stats':
handlePtrCacheStats($db);
break;
default:
jsonResponse(['error' => 'Invalid action'], 400);
}
@@ -2556,3 +2572,280 @@ function awsRoute53Request($method, $path, $body, $accessKeyId, $secretAccessKey
return json_decode(json_encode($xml), true);
}
/**
* Get cached PTR records for a zone
*/
function handlePtrCacheGet($db) {
$zoneId = $_GET['zone_id'] ?? '';
if (empty($zoneId)) {
jsonResponse(['error' => 'Zone ID required'], 400);
}
try {
$stmt = $db->prepare("
SELECT id, zone_id, zone_name, hostname, ip_address, ttl,
ptr_record, ptr_status, ptr_checked_at, aws_synced_at
FROM ptr_records_cache
WHERE zone_id = ?
ORDER BY hostname ASC
");
$stmt->execute([$zoneId]);
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Get last sync time
$stmt = $db->prepare("SELECT MAX(aws_synced_at) as last_sync FROM ptr_records_cache WHERE zone_id = ?");
$stmt->execute([$zoneId]);
$lastSync = $stmt->fetch(PDO::FETCH_ASSOC)['last_sync'];
jsonResponse([
'success' => true,
'records' => $records,
'count' => count($records),
'last_sync' => $lastSync
]);
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
}
}
/**
* Refresh PTR cache from AWS Route53
*/
function handlePtrCacheRefresh($db) {
$zoneId = $_GET['zone_id'] ?? '';
if (empty($zoneId)) {
jsonResponse(['error' => 'Zone ID required'], 400);
}
$accessKeyId = getSetting($db, 'aws_access_key_id', '');
$secretAccessKey = getSetting($db, 'aws_secret_access_key', '');
$region = getSetting($db, 'aws_region', 'us-east-1');
if (empty($accessKeyId) || empty($secretAccessKey)) {
jsonResponse(['error' => 'AWS credentials not configured'], 400);
}
try {
// Clean zone ID (remove /hostedzone/ prefix if present)
$cleanZoneId = preg_replace('/^\/hostedzone\//', '', $zoneId);
// Get zone name first
$zoneResult = awsRoute53Request('GET', "2013-04-01/hostedzone/{$cleanZoneId}", [], $accessKeyId, $secretAccessKey, $region);
$zoneName = '';
if (isset($zoneResult['HostedZone']['Name'])) {
$zoneName = rtrim($zoneResult['HostedZone']['Name'], '.');
}
// Get records from Route53
$result = awsRoute53Request('GET', "2013-04-01/hostedzone/{$cleanZoneId}/rrset?type=A", [], $accessKeyId, $secretAccessKey, $region);
if (isset($result['error'])) {
jsonResponse(['success' => false, 'error' => $result['error']]);
}
$records = [];
$syncTime = date('Y-m-d H:i:s');
if (isset($result['ResourceRecordSets']['ResourceRecordSet'])) {
$rrsets = $result['ResourceRecordSets']['ResourceRecordSet'];
// Handle single record case
if (isset($rrsets['Name'])) {
$rrsets = [$rrsets];
}
foreach ($rrsets as $rrset) {
if (($rrset['Type'] ?? '') !== 'A') continue;
$hostname = rtrim($rrset['Name'] ?? '', '.');
$ttl = $rrset['TTL'] ?? null;
// Get IPs from ResourceRecords
$ips = [];
if (isset($rrset['ResourceRecords']['ResourceRecord'])) {
$rrs = $rrset['ResourceRecords']['ResourceRecord'];
if (isset($rrs['Value'])) {
$ips[] = $rrs['Value'];
} else {
foreach ($rrs as $rr) {
if (isset($rr['Value'])) {
$ips[] = $rr['Value'];
}
}
}
}
foreach ($ips as $ip) {
$records[] = [
'zone_id' => $cleanZoneId,
'zone_name' => $zoneName,
'hostname' => $hostname,
'ip_address' => $ip,
'ttl' => $ttl
];
}
}
}
// Update database - use INSERT ... ON DUPLICATE KEY UPDATE
$inserted = 0;
$updated = 0;
foreach ($records as $record) {
$stmt = $db->prepare("
INSERT INTO ptr_records_cache
(zone_id, zone_name, hostname, ip_address, ttl, aws_synced_at)
VALUES (?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
zone_name = VALUES(zone_name),
ip_address = VALUES(ip_address),
ttl = VALUES(ttl),
aws_synced_at = VALUES(aws_synced_at)
");
$stmt->execute([
$record['zone_id'],
$record['zone_name'],
$record['hostname'],
$record['ip_address'],
$record['ttl'],
$syncTime
]);
if ($stmt->rowCount() === 1) {
$inserted++;
} else {
$updated++;
}
}
// Remove stale records that no longer exist in Route53
$stmt = $db->prepare("DELETE FROM ptr_records_cache WHERE zone_id = ? AND aws_synced_at < ?");
$stmt->execute([$cleanZoneId, $syncTime]);
$deleted = $stmt->rowCount();
jsonResponse([
'success' => true,
'message' => "Synced {$inserted} new, {$updated} updated, {$deleted} removed",
'count' => count($records),
'inserted' => $inserted,
'updated' => $updated,
'deleted' => $deleted,
'last_sync' => $syncTime
]);
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
}
}
/**
* Check all PTR records for a zone
*/
function handlePtrCheckAll($db) {
$zoneId = $_GET['zone_id'] ?? '';
if (empty($zoneId)) {
jsonResponse(['error' => 'Zone ID required'], 400);
}
try {
$cleanZoneId = preg_replace('/^\/hostedzone\//', '', $zoneId);
// Get all records for this zone
$stmt = $db->prepare("SELECT id, hostname, ip_address FROM ptr_records_cache WHERE zone_id = ?");
$stmt->execute([$cleanZoneId]);
$records = $stmt->fetchAll(PDO::FETCH_ASSOC);
$checked = 0;
$matches = 0;
$mismatches = 0;
$missing = 0;
$errors = 0;
foreach ($records as $record) {
$ip = $record['ip_address'];
$expectedHostname = $record['hostname'];
// Perform PTR lookup
$ptr = @gethostbyaddr($ip);
$ptrRecord = null;
$status = 'unknown';
if ($ptr === false || $ptr === $ip) {
$status = 'missing';
$missing++;
} else {
$ptrRecord = $ptr;
// Normalize for comparison
$normalizedPtr = strtolower(rtrim($ptr, '.'));
$normalizedExpected = strtolower(rtrim($expectedHostname, '.'));
if ($normalizedPtr === $normalizedExpected) {
$status = 'match';
$matches++;
} else {
$status = 'mismatch';
$mismatches++;
}
}
// Update the record
$stmt = $db->prepare("
UPDATE ptr_records_cache
SET ptr_record = ?, ptr_status = ?, ptr_checked_at = NOW()
WHERE id = ?
");
$stmt->execute([$ptrRecord, $status, $record['id']]);
$checked++;
}
jsonResponse([
'success' => true,
'checked' => $checked,
'matches' => $matches,
'mismatches' => $mismatches,
'missing' => $missing,
'errors' => $errors
]);
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
}
}
/**
* Get PTR cache stats
*/
function handlePtrCacheStats($db) {
try {
$stmt = $db->query("
SELECT
COUNT(*) as total,
SUM(CASE WHEN ptr_status = 'match' THEN 1 ELSE 0 END) as matches,
SUM(CASE WHEN ptr_status = 'mismatch' THEN 1 ELSE 0 END) as mismatches,
SUM(CASE WHEN ptr_status = 'missing' THEN 1 ELSE 0 END) as missing,
SUM(CASE WHEN ptr_status = 'unknown' THEN 1 ELSE 0 END) as unchecked,
MAX(aws_synced_at) as last_aws_sync,
MAX(ptr_checked_at) as last_ptr_check
FROM ptr_records_cache
");
$stats = $stmt->fetch(PDO::FETCH_ASSOC);
// Get per-zone breakdown
$stmt = $db->query("
SELECT zone_id, zone_name, COUNT(*) as count, MAX(aws_synced_at) as last_sync
FROM ptr_records_cache
GROUP BY zone_id, zone_name
ORDER BY zone_name
");
$zones = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonResponse([
'success' => true,
'stats' => $stats,
'zones' => $zones
]);
} catch (Exception $e) {
jsonResponse(['success' => false, 'error' => $e->getMessage()]);
}
}

View File

@@ -2103,7 +2103,7 @@ if (function_exists('requireAuth')) {
</svg>
PTR Record Management
</h2>
<p class="advanced-section-desc">View A records from your Route53 hosted zones and check their PTR records at IPXO.</p>
<p class="advanced-section-desc">View A records from your Route53 hosted zones and check their PTR records. Records are cached locally for faster access.</p>
<div id="ptrNotConfigured" style="display: none;">
<div class="alert alert-warning">
@@ -2117,21 +2117,24 @@ if (function_exists('requireAuth')) {
</div>
<div id="ptrConfigured">
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 20px; align-items: center;">
<!-- Zone selector and main action buttons -->
<div style="display: flex; gap: 12px; flex-wrap: wrap; margin-bottom: 16px; align-items: flex-end;">
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
<label class="form-label">Select Hosted Zone</label>
<select class="form-select" id="ptrZoneSelect" onchange="loadPtrRecords()">
<select class="form-select" id="ptrZoneSelect" onchange="loadCachedPtrRecords()">
<option value="">Select a zone...</option>
</select>
</div>
<button class="btn btn-primary" onclick="loadPtrRecords()" style="margin-top: 24px;">
<button class="btn btn-primary" onclick="refreshFromAws()" id="refreshAwsBtn" disabled title="Sync A records from AWS Route53">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10"/>
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
<path d="M16 21h5v-5"/>
</svg>
Refresh
Sync from AWS
</button>
<button class="btn btn-secondary" onclick="checkAllPtrs()" style="margin-top: 24px;" id="checkAllPtrsBtn" disabled>
<button class="btn btn-secondary" onclick="checkAllPtrs()" id="checkAllPtrsBtn" disabled title="Check PTR records for all IPs">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
@@ -2139,6 +2142,24 @@ if (function_exists('requireAuth')) {
</button>
</div>
<!-- Sync status bar -->
<div id="ptrSyncStatus" style="display: none; margin-bottom: 16px; padding: 12px 16px; background: var(--bg-tertiary); border-radius: var(--radius-md); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 12px;">
<div style="display: flex; gap: 16px; flex-wrap: wrap;">
<span style="font-size: 13px; color: var(--text-secondary);">
<strong>Last AWS Sync:</strong> <span id="lastAwsSync">-</span>
</span>
<span style="font-size: 13px; color: var(--text-secondary);">
<strong>Last PTR Check:</strong> <span id="lastPtrCheck">-</span>
</span>
</div>
<div id="ptrStatsBar" style="display: flex; gap: 12px; flex-wrap: wrap;">
<span style="font-size: 12px; padding: 2px 8px; border-radius: 4px; background: var(--success); color: white;" id="statMatch">0 Match</span>
<span style="font-size: 12px; padding: 2px 8px; border-radius: 4px; background: var(--warning); color: white;" id="statMismatch">0 Mismatch</span>
<span style="font-size: 12px; padding: 2px 8px; border-radius: 4px; background: var(--error); color: white;" id="statMissing">0 Missing</span>
<span style="font-size: 12px; padding: 2px 8px; border-radius: 4px; background: var(--text-tertiary); color: white;" id="statUnknown">0 Unchecked</span>
</div>
</div>
<div id="ptrRecordsContainer">
<div class="table-container">
<div class="table-header">
@@ -2154,7 +2175,7 @@ if (function_exists('requireAuth')) {
<th style="width: 80px;">TTL</th>
<th style="min-width: 250px;">Current PTR</th>
<th style="width: 100px;">PTR Status</th>
<th style="width: 100px;">Actions</th>
<th style="width: 120px;">Actions</th>
</tr>
</thead>
<tbody id="ptrRecordsBody">
@@ -4456,6 +4477,7 @@ if (function_exists('requireAuth')) {
const select = document.getElementById('ptrZoneSelect');
const notConfigured = document.getElementById('ptrNotConfigured');
const configured = document.getElementById('ptrConfigured');
const refreshAwsBtn = document.getElementById('refreshAwsBtn');
try {
const result = await api('aws_zones');
@@ -4481,12 +4503,16 @@ if (function_exists('requireAuth')) {
}
}
// Load A records from selected zone
async function loadPtrRecords() {
// Load cached PTR records from database
async function loadCachedPtrRecords() {
const zoneId = document.getElementById('ptrZoneSelect').value;
const tbody = document.getElementById('ptrRecordsBody');
const countSpan = document.getElementById('ptrRecordCount');
const checkAllBtn = document.getElementById('checkAllPtrsBtn');
const refreshAwsBtn = document.getElementById('refreshAwsBtn');
const syncStatus = document.getElementById('ptrSyncStatus');
refreshAwsBtn.disabled = !zoneId;
if (!zoneId) {
tbody.innerHTML = `
@@ -4498,6 +4524,7 @@ if (function_exists('requireAuth')) {
`;
countSpan.textContent = '';
checkAllBtn.disabled = true;
syncStatus.style.display = 'none';
return;
}
@@ -4510,34 +4537,63 @@ if (function_exists('requireAuth')) {
`;
try {
const result = await api('aws_records', { zone_id: zoneId });
const result = await api('ptr_cache_get', { zone_id: zoneId });
if (result.success) {
const records = result.records || [];
countSpan.textContent = records.length + ' record(s)';
checkAllBtn.disabled = records.length === 0;
// Update sync status
if (records.length > 0) {
syncStatus.style.display = 'flex';
document.getElementById('lastAwsSync').textContent = result.last_sync ? formatDateTime(result.last_sync) : 'Never';
// Calculate stats
let matches = 0, mismatches = 0, missing = 0, unknown = 0;
let lastPtrCheck = null;
records.forEach(r => {
if (r.ptr_status === 'match') matches++;
else if (r.ptr_status === 'mismatch') mismatches++;
else if (r.ptr_status === 'missing') missing++;
else unknown++;
if (r.ptr_checked_at && (!lastPtrCheck || r.ptr_checked_at > lastPtrCheck)) {
lastPtrCheck = r.ptr_checked_at;
}
});
document.getElementById('lastPtrCheck').textContent = lastPtrCheck ? formatDateTime(lastPtrCheck) : 'Never';
document.getElementById('statMatch').textContent = matches + ' Match';
document.getElementById('statMismatch').textContent = mismatches + ' Mismatch';
document.getElementById('statMissing').textContent = missing + ' Missing';
document.getElementById('statUnknown').textContent = unknown + ' Unchecked';
} else {
syncStatus.style.display = 'none';
}
if (records.length === 0) {
tbody.innerHTML = `
<tr>
<td colspan="6" style="text-align: center; color: var(--text-tertiary); padding: 40px;">
No A records found in this zone
No cached records. Click "Sync from AWS" to fetch A records.
</td>
</tr>
`;
return;
}
tbody.innerHTML = records.map((record, index) => `
<tr data-ip="${escapeHtml(record.ip)}" data-hostname="${escapeHtml(record.hostname)}">
tbody.innerHTML = records.map((record) => {
const statusColor = getStatusColor(record.ptr_status);
const statusLabel = (record.ptr_status || 'unknown').toUpperCase();
return `
<tr data-id="${record.id}" data-ip="${escapeHtml(record.ip_address)}" data-hostname="${escapeHtml(record.hostname)}">
<td><span class="cell-truncate" title="${escapeHtml(record.hostname)}">${escapeHtml(record.hostname)}</span></td>
<td><code style="font-size: 12px;">${escapeHtml(record.ip)}</code></td>
<td><code style="font-size: 12px;">${escapeHtml(record.ip_address)}</code></td>
<td style="color: var(--text-secondary);">${record.ttl || '-'}</td>
<td class="ptr-value" style="color: var(--text-tertiary);">-</td>
<td class="ptr-value" style="color: ${record.ptr_record ? 'var(--text-primary)' : 'var(--text-tertiary)'};">${record.ptr_record || '-'}</td>
<td class="ptr-status">
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: var(--text-tertiary); color: white;">UNKNOWN</span>
<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${statusLabel}</span>
</td>
<td>
<button class="btn btn-secondary btn-sm" onclick="checkPtrRecord('${escapeHtml(record.ip)}', '${escapeHtml(record.hostname)}', this)" title="Check PTR">
<button class="btn btn-secondary btn-sm" onclick="checkSinglePtr(${record.id}, this)" title="Check PTR">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>
@@ -4545,7 +4601,7 @@ if (function_exists('requireAuth')) {
</button>
</td>
</tr>
`).join('');
`}).join('');
} else {
tbody.innerHTML = `
<tr>
@@ -4556,6 +4612,7 @@ if (function_exists('requireAuth')) {
`;
countSpan.textContent = '';
checkAllBtn.disabled = true;
syncStatus.style.display = 'none';
}
} catch (error) {
tbody.innerHTML = `
@@ -4566,15 +4623,71 @@ if (function_exists('requireAuth')) {
</tr>
`;
countSpan.textContent = '';
syncStatus.style.display = 'none';
checkAllBtn.disabled = true;
}
}
// Check single PTR record
async function checkPtrRecord(ip, expectedHostname, button) {
// Helper function to get status color
function getStatusColor(status) {
switch (status) {
case 'match': return 'var(--success)';
case 'mismatch': return 'var(--warning)';
case 'missing': return 'var(--error)';
case 'error': return 'var(--error)';
default: return 'var(--text-tertiary)';
}
}
// Format date time for display
function formatDateTime(dateStr) {
if (!dateStr) return '-';
const date = new Date(dateStr);
return date.toLocaleString();
}
// Refresh from AWS - sync A records from Route53
async function refreshFromAws() {
const zoneId = document.getElementById('ptrZoneSelect').value;
if (!zoneId) {
showToast('Please select a zone first', 'warning');
return;
}
const refreshAwsBtn = document.getElementById('refreshAwsBtn');
refreshAwsBtn.disabled = true;
refreshAwsBtn.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div> Syncing...';
try {
const result = await api('ptr_cache_refresh', { zone_id: zoneId });
if (result.success) {
showToast(`AWS sync complete: ${result.message}`, 'success');
await loadCachedPtrRecords();
} else {
showToast(result.error || 'Failed to sync from AWS', 'error');
}
} catch (error) {
showToast('Failed to sync from AWS: ' + error.message, 'error');
}
refreshAwsBtn.disabled = false;
refreshAwsBtn.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/>
<path d="M3 3v5h5"/>
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16"/>
<path d="M16 21h5v-5"/>
</svg>
Sync from AWS
`;
}
// Check single PTR record (for cached records)
async function checkSinglePtr(recordId, button) {
const row = button.closest('tr');
const ptrValueCell = row.querySelector('.ptr-value');
const ptrStatusCell = row.querySelector('.ptr-status');
const ip = row.getAttribute('data-ip');
button.disabled = true;
button.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div>';
@@ -4586,23 +4699,11 @@ if (function_exists('requireAuth')) {
ptrValueCell.textContent = ptr || '(none)';
ptrValueCell.style.color = ptr ? 'var(--text-primary)' : 'var(--text-tertiary)';
// Normalize hostnames for comparison (remove trailing dot, lowercase)
const normalizedPtr = ptr.toLowerCase().replace(/\.$/, '');
const normalizedExpected = expectedHostname.toLowerCase().replace(/\.$/, '');
const status = result.status || 'unknown';
const statusColor = getStatusColor(status);
const statusLabel = status.toUpperCase();
let status, statusColor;
if (!ptr) {
status = 'MISSING';
statusColor = 'var(--error)';
} else if (normalizedPtr === normalizedExpected) {
status = 'MATCH';
statusColor = 'var(--success)';
} else {
status = 'MISMATCH';
statusColor = 'var(--warning)';
}
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${status}</span>`;
ptrStatusCell.innerHTML = `<span style="display: inline-block; padding: 4px 8px; border-radius: 4px; font-size: 11px; font-weight: 600; background: ${statusColor}; color: white;">${statusLabel}</span>`;
} else {
ptrValueCell.textContent = 'Error';
ptrValueCell.style.color = 'var(--error)';
@@ -4623,30 +4724,28 @@ if (function_exists('requireAuth')) {
`;
}
// Check all PTR records
// Check all PTR records (batch via API)
async function checkAllPtrs() {
const rows = document.querySelectorAll('#ptrRecordsBody tr[data-ip]');
const checkAllBtn = document.getElementById('checkAllPtrsBtn');
if (rows.length === 0) {
showToast('No records to check', 'warning');
const zoneId = document.getElementById('ptrZoneSelect').value;
if (!zoneId) {
showToast('Please select a zone first', 'warning');
return;
}
const checkAllBtn = document.getElementById('checkAllPtrsBtn');
checkAllBtn.disabled = true;
checkAllBtn.innerHTML = '<div class="spinner" style="width: 14px; height: 14px;"></div> Checking...';
let checked = 0;
for (const row of rows) {
const ip = row.getAttribute('data-ip');
const hostname = row.getAttribute('data-hostname');
const button = row.querySelector('button');
await checkPtrRecord(ip, hostname, button);
checked++;
// Small delay to avoid overwhelming the server
await new Promise(resolve => setTimeout(resolve, 100));
try {
const result = await api('ptr_check_all', { zone_id: zoneId });
if (result.success) {
showToast(`Checked ${result.checked} PTR(s): ${result.matches} match, ${result.mismatches} mismatch, ${result.missing} missing`, 'success');
await loadCachedPtrRecords();
} else {
showToast(result.error || 'Failed to check PTRs', 'error');
}
} catch (error) {
showToast('Failed to check PTRs: ' + error.message, 'error');
}
checkAllBtn.disabled = false;
@@ -4656,8 +4755,6 @@ if (function_exists('requireAuth')) {
</svg>
Check All PTRs
`;
showToast(`Checked ${checked} PTR record(s)`, 'success');
}
// Clear error logs