Main_Website-Oblistudios/ASAservers.html

613 lines
21 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>ObliStudios · ASA Servers — Live Status</title>
<meta name="description" content="Live status for ObliStudios' ARK: Survival Ascended servers." />
<meta name="theme-color" content="#10e39a" />
<meta property="og:title" content="ObliStudios · ASA Servers — Live Status" />
<meta property="og:description" content="Realtime online status, map, players, and ping for every ObliStudios ASA server." />
<meta property="og:type" content="website" />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&family=Cinzel:wght@600;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #0a0b10;
--panel: #111421;
--panel-2: #0d1020;
--text: #e8eef6;
--muted: #9aa6b2;
--line: rgba(255,255,255,.08);
--accent: #10e39a;
--accent2: #0dc07f;
--warn: #f59e0b;
--bad: #ff3b3b;
--radius: 14px;
--shadow: 0 12px 28px rgba(0,0,0,.35);
}
* {
box-sizing: border-box
}
html, body {
height: 100%
}
body {
margin: 0;
font: 16px/1.6 Inter,system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
color: var(--text);
background: url('img/BestWCoast.png') no-repeat center center fixed;
background-size: cover;
}
/* Soft vignette */
body::before {
content: "";
position: fixed;
inset: 0;
background: linear-gradient( rgba(0,0,0,0.72) 0%, rgba(0,0,0,0.55) 30%, rgba(0,0,0,0.38) 60%, rgba(0,0,0,0.62) 100% );
z-index: -1;
}
a {
color: inherit;
text-decoration: none
}
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px
}
/* Header (harmonized with other pages) */
header {
position: sticky;
top: 0;
z-index: 50;
backdrop-filter: saturate(180%) blur(8px);
background: rgba(10,11,16,.6);
border-bottom: 1px solid var(--line);
}
.nav {
height: 68px;
display: flex;
align-items: center;
justify-content: space-between
}
.brand {
display: flex;
gap: .65rem;
align-items: center
}
.brand svg {
width: 30px;
height: 30px;
filter: drop-shadow(0 0 10px rgba(16,227,154,.4))
}
.wordmark {
font-weight: 800;
letter-spacing: .2px
}
.wordmark em {
color: var(--accent);
font-style: normal
}
.links {
display: flex;
gap: 18px;
color: var(--muted);
font-weight: 600
}
.links a:hover {
color: var(--text)
}
/* Hero */
h1, h2 {
font-family: Cinzel, Inter, serif
}
.hero {
padding: 64px 0 24px;
}
h1 {
margin: .35rem 0 .4rem;
line-height: 1.15;
font-size: clamp(2rem, 1rem + 3vw, 3rem)
}
.lead {
color: var(--muted);
max-width: 70ch
}
.notice {
font-size: .9rem;
color: #cfd7e0
}
/* Controls bar */
.controls {
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
padding: 12px;
margin: 8px 0 18px;
background: rgba(17,20,33,.75);
border: 1px solid rgba(255,255,255,.08);
border-radius: 12px;
backdrop-filter: blur(6px);
}
.controls input, .controls select, .controls button {
background: #0a0c12;
color: var(--text);
border: 1px solid rgba(255,255,255,.08);
border-radius: 10px;
padding: .55rem .7rem;
font-weight: 600;
}
.controls button.primary {
background: linear-gradient(135deg,var(--accent),var(--accent2));
color: #00140d;
border: none;
box-shadow: 0 8px 22px rgba(16,227,154,.25);
}
.controls .meta {
color: var(--muted);
font-size: .95rem;
margin-left: auto;
display: flex;
gap: 12px;
align-items: center
}
/* Grid & cards */
.grid {
display: grid;
gap: 18px
}
@media (min-width:760px) {
.grid {
grid-template-columns: 1fr 1fr
}
}
.server-card {
background: rgba(17, 20, 33, 0.85);
border: 1px solid rgba(255,255,255,0.12);
border-radius: var(--radius);
backdrop-filter: blur(6px);
padding: 14px 16px;
box-shadow: var(--shadow);
}
.server-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.pill {
display: inline-flex;
align-items: center;
gap: .4rem;
padding: .25rem .6rem;
border-radius: 999px;
font-weight: 800;
font-size: .8rem
}
.up {
background: #0dc07f22;
border: 1px solid #0dc07f66;
color: #b6f0dc
}
.down {
background: #ff3b3b22;
border: 1px solid #ff3b3b66;
color: #ffc9c9
}
.muted {
color: var(--muted)
}
.ephemeral {
font-size: .9rem;
color: var(--muted)
}
.row {
display: flex;
gap: 12px;
align-items: center;
flex-wrap: wrap
}
.copy {
cursor: pointer;
border: 1px solid var(--line);
border-radius: 10px;
padding: .35rem .55rem;
font-weight: 700
}
.kvs {
display: grid;
grid-template-columns: repeat(3,1fr);
gap: 8px 14px;
margin-top: .5rem
}
.kv strong {
display: block;
font-size: .9rem;
color: var(--muted)
}
.kv span {
font-weight: 800
}
/* Player capacity bar */
.bar {
height: 8px;
border-radius: 999px;
background: #0a0c12;
border: 1px solid rgba(255,255,255,.08);
overflow: hidden
}
.bar > i {
display: block;
height: 100%;
background: linear-gradient(90deg,var(--accent),var(--accent2));
width: 0%
}
/* Ping chip */
.ping {
display: inline-flex;
align-items: center;
gap: .35rem;
font-weight: 800
}
.dot {
width: 10px;
height: 10px;
border-radius: 50%
}
/* Skeletons */
.skeleton {
position: relative;
overflow: hidden;
border-radius: var(--radius);
background: rgba(255,255,255,.06);
height: 110px;
border: 1px solid rgba(255,255,255,.08)
}
.skeleton::after {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,.08), transparent);
transform: translateX(-100%);
animation: shimmer 1.4s infinite;
}
@keyframes shimmer {
100% {
transform: translateX(100%)
}
}
/* Footer */
.footer {
padding: 40px 0 64px;
color: var(--muted);
border-top: 1px solid var(--line)
}
[hidden] {
display: none !important
}
</style>
</head>
<body>
<header>
<div class="container nav" aria-label="Main">
<a class="brand" href="/index.html" aria-label="Home">
<svg viewBox="0 0 64 64" aria-hidden="true" role="img">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#10e39a" />
<stop offset="100%" stop-color="#0dc07f" />
</linearGradient>
</defs>
<path fill="url(#g)" d="M32 6c11 0 20 8 20 20s-9 22-20 22S12 38 12 26 21 6 32 6Z" />
<ellipse cx="24" cy="28" rx="7" ry="5" fill="#06090f" />
<ellipse cx="40" cy="28" rx="7" ry="5" fill="#06090f" />
</svg>
<div class="wordmark">Obli<strong><em>Studios</em></strong></div>
</a>
<nav class="links" aria-label="Primary">
<a aria-current="page" href="https://www.oblistudios.com">Home</a>
<a aria-current="page" href="https://www.oblistudios.com/ASAshop.html"> Shop </a>
</nav>
</div>
</header>
<section class="hero">
<div class="container">
<h1>ARK: Survival Ascended — Live Server Status</h1>
<p class="lead">Realtime online status, map, players, and ping for every ObliStudios ASA server.</p>
<div class="notice">This is unofficial and not affiliated with Studio Wildcard.</div>
</div>
</section>
<!-- Controls -->
<div class="container">
<div class="controls" role="region" aria-label="Filters and actions">
<input id="q" type="search" placeholder="Search by name or map…" aria-label="Search" />
<select id="filter" aria-label="Filter by status">
<option value="all">All</option>
<option value="online">Online</option>
<option value="offline">Offline</option>
</select>
<select id="sort" aria-label="Sort">
<option value="status">Sort: Status</option>
<option value="name">Sort: Name A→Z</option>
<option value="players">Sort: Players</option>
<option value="ping">Sort: Ping</option>
</select>
<button id="refreshBtn" class="primary" type="button" aria-label="Refresh now">Refresh</button>
<div class="meta">
<span id="summary" aria-live="polite"></span>
<span id="nextRefresh" class="muted">Next update: —</span>
</div>
</div>
</div>
<main class="container" style="padding:8px 0 42px">
<h2 style="margin:0 0 10px">Cluster Status</h2>
<div id="servers" class="grid" aria-live="polite">
<!-- Skeletons while first load -->
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
<div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div><div class="skeleton"></div>
</div>
<div class="notice" style="margin-top:22px">
The servers are running on a besteffort basis, 24/7. Occasional downtime may occur for maintenance, updates, or unexpected issues.
Visit our <h1><a href="https://discord.gg/Dvkr3cK25U">Discord</a></h1> for planned maintenance windows and updates.
</div>
<!-- Toast / a11y region for copy feedback -->
<div id="toast" class="ephemeral" aria-live="polite" style="margin-top:10px"></div>
</main>
<footer class="footer">
<div class="container">
<small>© <span id="y"></span> ObliStudios. All rights reserved.</small>
</div>
</footer>
<script>
// Year stamp
document.getElementById('y').textContent = new Date().getFullYear();
// === CONFIG (same API + servers as your current page) ===
const API = 'https://flush-flush-slow-managed.trycloudflare.com';
const SERVERS = [
{ name: 'The Island (PvP)', host: '10.1.10.64', port: 27015 },
{ name: 'The Center (PvP)', host: '10.1.10.64', port: 27016 },
{ name: 'Scorched Earth (PvP)', host: '10.1.10.64', port: 27017 },
{ name: 'Aberration (PvP)', host: '10.1.10.64', port: 27018 },
{ name: 'Extinction (PvP)', host: '10.1.10.64', port: 27019 },
{ name: 'Ragnarok (PvP)', host: '10.1.10.64', port: 27020 },
{ name: 'Asteraeos (PvP)', host: '10.1.10.64', port: 27021 },
{ name: 'Ark Club', host: '10.1.10.64', port: 27022 },
{ name: 'Server 9', host: '10.1.10.64', port: 27023 }
];
// === STATE ===
let state = []; // { s, data, fetchedAt }
let lastRefresh = 0;
let nextTick = 30; // seconds
const $list = document.getElementById('servers');
const $toast = document.getElementById('toast');
const $summary = document.getElementById('summary');
const $next = document.getElementById('nextRefresh');
// === HELPERS ===
function timeAgo(ts) {
if (!ts) return '—';
const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
if (s < 5) return 'just now';
const units = [['d', 86400], ['h', 3600], ['m', 60], ['s', 1]];
for (const [u, v] of units) if (s >= v) return `${Math.floor(s / v)}${u} ago`;
return '—';
}
function pct(a, b) { return (!a || !b) ? 0 : Math.max(0, Math.min(100, Math.round((a / b) * 100))); }
function pingDot(p) {
let c = '#b6f0dc'; // default
if (typeof p === 'number') {
if (p <= 60) c = '#20df9b';
else if (p <= 120) c = '#f3c969';
else c = '#ff6d6d';
}
return `<span class="dot" style="background:${c}"></span>`;
}
function clip(txt, msg = 'Copied') {
navigator.clipboard.writeText(txt).then(() => {
$toast.textContent = `${msg}: ${txt}`;
setTimeout(() => { $toast.textContent = ''; }, 1800);
}).catch(() => {
$toast.textContent = 'Copy failed';
setTimeout(() => { $toast.textContent = ''; }, 1800);
});
}
// === TEMPLATES ===
function cardTemplate(s, data, ts) {
const online = !!(data && data.online);
const pill = online
? '<span class="pill up">Online</span>'
: '<span class="pill down">Offline</span>';
const map = data?.map || '—';
const players = Number.isFinite(data?.players) ? data.players : 0;
const maxPlayers = Number.isFinite(data?.maxPlayers) ? data.maxPlayers : null;
const ping = Number.isFinite(data?.ping) ? data.ping : null;
const endpoint = `${s.host}:${s.port}`;
const playerBar = Number.isFinite(maxPlayers) ? `
<div class="bar" aria-hidden="true"><i style="width:${pct(players, maxPlayers)}%"></i></div>
<span class="muted" style="font-size:.9rem">${pct(players, maxPlayers)}% capacity</span>
` : '';
const details = online ? `
<div class="kvs">
<div class="kv"><strong>Map</strong><span>${map}</span></div>
<div class="kv"><strong>Players</strong><span>${players}${maxPlayers ? `/${maxPlayers}` : ''}</span></div>
<div class="kv"><strong>Ping</strong><span class="ping">${pingDot(ping)}${ping ?? '—'} ms</span></div>
</div>
${playerBar}
` : `<div class="muted" style="margin-top:.3rem">${(data && data.error) ? data.error : 'No response from query port'}</div>`;
return `
<article class="server-card" role="region" aria-label="${s.name} status">
<div class="server-head">
<h3 style="margin:.1rem 0 .2rem">${s.name}</h3>
${pill}
</div>
<div class="row muted" style="margin-bottom:.4rem">
<strong>Query:</strong> <code>${endpoint}</code>
<button class="copy" onclick="clip('${endpoint}','Copied endpoint')">Copy</button>
</div>
${details}
<div class="muted" style="margin-top:.6rem;font-size:.9rem">
Updated <span>${timeAgo(ts)}</span>
</div>
</article>
`;
}
function render() {
const q = document.getElementById('q').value.trim().toLowerCase();
const filter = document.getElementById('filter').value;
const sort = document.getElementById('sort').value;
let rows = state.slice();
// Filter
rows = rows.filter(({ s, data }) => {
const hay = `${s.name} ${data?.map || ''}`.toLowerCase();
const matchesQ = !q || hay.includes(q);
const online = !!data?.online;
const matchesF = filter === 'all' || (filter === 'online' ? online : !online);
return matchesQ && matchesF;
});
// Sort
const by = {
status: (a, b) => Number(b.data?.online || 0) - Number(a.data?.online || 0) || a.s.name.localeCompare(b.s.name),
name: (a, b) => a.s.name.localeCompare(b.s.name),
players: (a, b) => (b.data?.players || 0) - (a.data?.players || 0),
ping: (a, b) => (a.data?.ping ?? 1e9) - (b.data?.ping ?? 1e9),
}[sort] || ((a, b) => 0);
rows.sort(by);
// Render
$list.innerHTML = rows.map(r => cardTemplate(r.s, r.data, r.fetchedAt)).join('');
// Summary / meta
const total = state.length;
const onlineCount = state.filter(r => r.data?.online).length;
$summary.textContent = `Online ${onlineCount} / ${total} · Last update ${timeAgo(lastRefresh)}`;
}
// === DATA FETCH ===
function fetchWithTimeout(url, ms = 7000) {
const ctl = new AbortController();
const id = setTimeout(() => ctl.abort(), ms);
return fetch(url, { cache: 'no-store', signal: ctl.signal }).finally(() => clearTimeout(id));
}
async function refresh() {
lastRefresh = Date.now();
nextTick = 30;
const results = await Promise.all(SERVERS.map(async s => {
const url = `${API}?ip=${encodeURIComponent(s.host)}&port=${encodeURIComponent(s.port)}`;
try {
const r = await fetchWithTimeout(url, 7000);
const data = await r.json();
return { s, data, fetchedAt: Date.now() };
} catch (e) {
return { s, data: { online: false, error: String(e) }, fetchedAt: Date.now() };
}
}));
state = results;
// Persist last successful for offline firstpaint
try { localStorage.setItem('asa:last', JSON.stringify({ t: Date.now(), results })); } catch { }
render();
}
// Load cached (if present) for instant first paint
(function bootFromCache() {
try {
const cached = JSON.parse(localStorage.getItem('asa:last') || 'null');
if (cached && Array.isArray(cached.results)) {
state = cached.results;
lastRefresh = cached.t || Date.now();
render();
}
} catch { }
})();
// Polling and countdown
setInterval(() => {
if (nextTick > 0) nextTick--;
$next.textContent = `Next update: ${nextTick}s`;
if (nextTick === 0) refresh();
}, 1000);
// Wire controls
document.getElementById('q').addEventListener('input', render);
document.getElementById('filter').addEventListener('change', render);
document.getElementById('sort').addEventListener('change', render);
document.getElementById('refreshBtn').addEventListener('click', refresh);
// First live refresh
refresh();
</script>
</body>
</html>