fix width
This commit is contained in:
@@ -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', ''),
|
||||
|
||||
293
webapp/api.php
293
webapp/api.php
@@ -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()]);
|
||||
}
|
||||
}
|
||||
|
||||
211
webapp/index.php
211
webapp/index.php
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user