|
<!DOCTYPE html> |
|
<html lang="ja"> |
|
<head> |
|
<meta charset="UTF-8" /> |
|
<title>管理ページ(IP・接続時間表示 + 地図表示)</title> |
|
<script src="https://cdn.socket.io/4.0.1/socket.io.min.js"></script> |
|
<link |
|
rel="stylesheet" |
|
href="https://unpkg.com/[email protected]/dist/leaflet.css" |
|
/> |
|
<script |
|
src="https://unpkg.com/[email protected]/dist/leaflet.js" |
|
></script> |
|
<style> |
|
table { border-collapse: collapse; width: 100%; margin-bottom: 20px;} |
|
th, td { border: 1px solid #ccc; padding: 8px; text-align: left; } |
|
th { background-color: #eee; } |
|
.highlight { |
|
background-color: #f99; |
|
transition: background-color 1s ease; |
|
} |
|
#map { |
|
height: 400px; |
|
width: 100%; |
|
margin-top: 20px; |
|
border: 1px solid #ccc; |
|
} |
|
</style> |
|
</head> |
|
<body> |
|
<h1>現在の接続ユーザー数: <span id="userCount">0</span></h1> |
|
<div>更新はページ再読み込みで行います。</div> |
|
<table> |
|
<thead> |
|
<tr> |
|
<th>Socket ID</th> |
|
<th>IPアドレス</th> |
|
<th>接続時間</th> |
|
<th>切断時間</th> |
|
</tr> |
|
</thead> |
|
<tbody id="userTableBody"></tbody> |
|
</table> |
|
|
|
<div id="map"></div> |
|
|
|
<script> |
|
const socket = io("https://web-socket-server-14ap.onrender.com/"); |
|
|
|
const STORAGE_KEY = "userSessions"; |
|
|
|
|
|
let previousSessions = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}"); |
|
|
|
const userCountElem = document.getElementById("userCount"); |
|
const tbody = document.getElementById("userTableBody"); |
|
const mapDiv = document.getElementById("map"); |
|
|
|
|
|
const map = L.map('map').setView([35.681236, 139.767125], 5); |
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { |
|
maxZoom: 18, |
|
attribution: '© OpenStreetMap contributors' |
|
}).addTo(map); |
|
|
|
|
|
let markers = {}; |
|
|
|
|
|
async function fetchGeo(ip) { |
|
if(!ip || ip === "-") return null; |
|
try { |
|
const response = await fetch(`https://ip-api.com/json/${ip}?fields=status,message,lat,lon`); |
|
const data = await response.json(); |
|
if(data.status === "success") { |
|
return { lat: data.lat, lon: data.lon }; |
|
} else { |
|
console.warn(`IP位置情報取得失敗: ${ip} - ${data.message}`); |
|
return null; |
|
} |
|
} catch (e) { |
|
console.error("IP位置情報取得エラー", e); |
|
return null; |
|
} |
|
} |
|
|
|
|
|
function createCircleMarker(lat, lon, color, popupText) { |
|
return L.circleMarker([lat, lon], { |
|
radius: 8, |
|
fillColor: color, |
|
color: '#000', |
|
weight: 1, |
|
opacity: 1, |
|
fillOpacity: 0.8 |
|
}).bindPopup(popupText); |
|
} |
|
|
|
socket.on("updateUserCount", (count) => { |
|
userCountElem.textContent = count; |
|
}); |
|
|
|
socket.on("userSessionsUpdate", async (userSessions) => { |
|
tbody.innerHTML = ""; |
|
|
|
|
|
for(let key in markers) { |
|
map.removeLayer(markers[key]); |
|
} |
|
markers = {}; |
|
|
|
|
|
const positions = {}; |
|
await Promise.all(Object.entries(userSessions).map(async ([socketId, info]) => { |
|
positions[socketId] = await fetchGeo(info.ip); |
|
})); |
|
|
|
for (const [socketId, info] of Object.entries(userSessions)) { |
|
const tr = document.createElement("tr"); |
|
|
|
function createTd(text, elementKey) { |
|
const td = document.createElement("td"); |
|
td.textContent = text; |
|
|
|
const prevValue = previousSessions[socketId] ? previousSessions[socketId][elementKey] : undefined; |
|
const currValue = info[elementKey]; |
|
|
|
let changed = prevValue !== currValue; |
|
|
|
if (changed) { |
|
td.classList.add("highlight"); |
|
setTimeout(() => td.classList.remove("highlight"), 1000); |
|
} |
|
|
|
return td; |
|
} |
|
|
|
tr.appendChild(createTd(socketId, "socketId")); |
|
tr.appendChild(createTd(info.ip || "-", "ip")); |
|
tr.appendChild(createTd(info.connectTime ? new Date(info.connectTime).toLocaleString() : "-", "connectTime")); |
|
tr.appendChild(createTd(info.disconnectTime ? new Date(info.disconnectTime).toLocaleString() : "-", "disconnectTime")); |
|
|
|
tbody.appendChild(tr); |
|
|
|
|
|
const pos = positions[socketId]; |
|
if(pos) { |
|
|
|
const prevPos = previousSessions[socketId]?.geoPosition; |
|
let changedPos = true; |
|
if(prevPos && Math.abs(prevPos.lat - pos.lat) < 0.0001 && Math.abs(prevPos.lon - pos.lon) < 0.0001) { |
|
changedPos = false; |
|
} |
|
|
|
|
|
const color = changedPos ? 'red' : 'black'; |
|
|
|
const marker = createCircleMarker(pos.lat, pos.lon, color, `SocketID: ${socketId}<br>IP: ${info.ip || "-"}`); |
|
marker.addTo(map); |
|
markers[socketId] = marker; |
|
} |
|
} |
|
|
|
|
|
const sessionsToStore = {}; |
|
for(const [socketId, info] of Object.entries(userSessions)) { |
|
sessionsToStore[socketId] = { |
|
...info, |
|
geoPosition: positions[socketId] || null |
|
}; |
|
} |
|
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(sessionsToStore)); |
|
previousSessions = sessionsToStore; |
|
}); |
|
|
|
|
|
if (Object.keys(previousSessions).length > 0) { |
|
socket.emit("userSessionsUpdate", previousSessions); |
|
} |
|
</script> |
|
</body> |
|
</html> |
|
|