Files
mdaleo404 8f60b5dcda
Build Docker Image & Security / build-scan-push (push) Successful in 2m5s
Remove PWA support
2026-07-07 15:33:02 +01:00

677 lines
19 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SysAdmin Arcade</title>
<link rel="icon" href="favicon/arcade-favicon.ico" sizes="any">
<link rel="icon" href="favicon/arcade-favicon-16.png" type="image/png" sizes="16x16">
<link rel="icon" href="favicon/arcade-favicon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="favicon/arcade-favicon-192.png" type="image/png" sizes="192x192">
<link rel="apple-touch-icon" href="favicon/arcade-favicon-180.png" sizes="180x180">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700;800&family=Space+Grotesk:wght@500;600;700&display=swap" rel="stylesheet">
<style>
:root{
--bg:#030712;
--panel:#08111f;
--panel-hi:#101d33;
--line:#203450;
--line-hot:#3ce7d8;
--text:#d7e8ff;
--muted:#7890b5;
--cyan:#45f0dd;
--blue:#62a8ff;
--green:#58f29a;
--amber:#ffbd5a;
--red:#ff5c72;
--shadow:rgba(0,0,0,0.45);
}
*{box-sizing:border-box;margin:0;padding:0;}
html{scroll-behavior:smooth;}
body{
min-height:100vh;
color:var(--text);
font-family:'JetBrains Mono', monospace;
background:
radial-gradient(circle at 20% 10%, rgba(69,240,221,0.18), transparent 28rem),
radial-gradient(circle at 82% 0%, rgba(98,168,255,0.16), transparent 30rem),
linear-gradient(180deg, #06101f 0%, var(--bg) 56%, #02040a 100%);
overflow-x:hidden;
}
body::before{
content:"";
position:fixed;
inset:0;
pointer-events:none;
background:
linear-gradient(rgba(69,240,221,0.035) 1px, transparent 1px),
linear-gradient(90deg, rgba(69,240,221,0.03) 1px, transparent 1px);
background-size:44px 44px;
mask-image:linear-gradient(to bottom, rgba(0,0,0,0.8), transparent 82%);
}
body::after{
content:"";
position:fixed;
inset:0;
pointer-events:none;
background:repeating-linear-gradient(180deg, rgba(255,255,255,0.028) 0 1px, transparent 1px 4px);
opacity:0.28;
mix-blend-mode:screen;
}
a{color:inherit;text-decoration:none;}
.shell{
width:min(1180px, calc(100% - 32px));
margin:0 auto;
padding:30px 0 42px;
position:relative;
z-index:1;
}
.topbar{
display:flex;
align-items:center;
justify-content:space-between;
gap:18px;
margin-bottom:48px;
color:var(--muted);
font-size:12px;
letter-spacing:0.16em;
text-transform:uppercase;
}
.brand{
display:flex;
align-items:center;
gap:10px;
color:#fff;
font-weight:800;
}
.brand-mark{
width:28px;
height:28px;
border:1px solid rgba(69,240,221,0.7);
border-radius:7px;
background:
linear-gradient(135deg, rgba(69,240,221,0.28), transparent),
var(--panel);
box-shadow:0 0 22px rgba(69,240,221,0.32), inset 0 0 16px rgba(69,240,221,0.14);
position:relative;
}
.brand-mark::before,
.brand-mark::after{
content:"";
position:absolute;
left:6px;
right:6px;
height:3px;
background:var(--cyan);
box-shadow:0 0 10px var(--cyan);
}
.brand-mark::before{top:7px;}
.brand-mark::after{bottom:7px;background:var(--green);box-shadow:0 0 10px var(--green);}
.status{
display:flex;
align-items:center;
gap:9px;
white-space:nowrap;
}
.status-dot{
width:9px;
height:9px;
border-radius:50%;
background:var(--green);
box-shadow:0 0 16px var(--green);
}
.hero{
display:grid;
grid-template-columns:minmax(0, 1.02fr) minmax(320px, 0.78fr);
gap:26px;
align-items:stretch;
}
.terminal,
.ops-card,
.game-card{
border:1px solid rgba(92,126,174,0.34);
background:linear-gradient(180deg, rgba(16,29,51,0.86), rgba(6,13,26,0.88));
box-shadow:0 28px 90px var(--shadow), inset 0 1px rgba(255,255,255,0.05);
backdrop-filter:blur(12px);
}
.terminal{
min-height:430px;
border-radius:22px;
overflow:hidden;
position:relative;
}
.terminal::before{
content:"";
position:absolute;
inset:auto -10% 0;
height:160px;
background:radial-gradient(ellipse at center, rgba(69,240,221,0.18), transparent 62%);
filter:blur(4px);
}
.window-bar{
height:44px;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 18px;
border-bottom:1px solid rgba(92,126,174,0.25);
background:rgba(3,7,18,0.42);
color:var(--muted);
font-size:11px;
letter-spacing:0.14em;
text-transform:uppercase;
}
.lights{display:flex;gap:7px;}
.light{width:10px;height:10px;border-radius:50%;background:var(--red);box-shadow:0 0 12px currentColor;}
.light:nth-child(2){background:var(--amber);}
.light:nth-child(3){background:var(--green);}
.terminal-body{
padding:34px;
position:relative;
}
.prompt{
color:var(--green);
font-weight:700;
margin-bottom:14px;
text-shadow:0 0 18px rgba(88,242,154,0.18);
}
h1{
font-family:'Space Grotesk', sans-serif;
font-size:clamp(48px, 8vw, 92px);
line-height:0.9;
letter-spacing:-0.06em;
max-width:720px;
color:#fff;
text-shadow:0 0 40px rgba(69,240,221,0.2);
}
.cursor{
color:var(--cyan);
animation:blink 1.05s steps(1) infinite;
}
@keyframes blink{50%{opacity:0;}}
.lead{
max-width:650px;
margin:24px 0 30px;
color:#a9bddb;
font-size:clamp(15px, 2vw, 18px);
line-height:1.75;
}
.cta-row{
display:flex;
flex-wrap:wrap;
gap:12px;
align-items:center;
}
.button{
display:inline-flex;
align-items:center;
justify-content:center;
min-height:50px;
padding:0 22px;
border-radius:12px;
border:1px solid rgba(69,240,221,0.68);
background:linear-gradient(135deg, rgba(69,240,221,0.22), rgba(98,168,255,0.12));
color:#fff;
font-weight:800;
letter-spacing:0.08em;
text-transform:uppercase;
box-shadow:0 0 34px rgba(69,240,221,0.18), inset 0 0 24px rgba(69,240,221,0.08);
transition:transform 160ms ease, border-color 160ms ease, box-shadow 160ms ease;
}
.button:hover{
transform:translateY(-2px);
border-color:var(--cyan);
box-shadow:0 16px 42px rgba(69,240,221,0.24), inset 0 0 28px rgba(69,240,221,0.12);
}
.button.secondary{
border-color:rgba(120,144,181,0.38);
background:rgba(8,17,31,0.58);
color:#c9dcfb;
box-shadow:none;
}
.side-stack{
display:grid;
gap:16px;
}
.ops-card{
border-radius:20px;
padding:22px;
overflow:hidden;
position:relative;
}
.ops-card::after{
content:"";
position:absolute;
width:190px;
height:190px;
right:-72px;
bottom:-72px;
border-radius:50%;
background:radial-gradient(circle, rgba(69,240,221,0.18), transparent 68%);
}
.ops-label,
.section-label{
color:var(--cyan);
font-size:11px;
letter-spacing:0.22em;
text-transform:uppercase;
font-weight:800;
margin-bottom:14px;
}
.metric-grid{
display:grid;
grid-template-columns:repeat(2, 1fr);
gap:12px;
position:relative;
z-index:1;
}
.metric{
min-height:92px;
border:1px solid rgba(92,126,174,0.25);
border-radius:14px;
padding:14px;
background:rgba(3,7,18,0.38);
}
.metric strong{
display:block;
color:#fff;
font-family:'Space Grotesk', sans-serif;
font-size:28px;
line-height:1;
margin-bottom:8px;
}
.metric span{
color:var(--muted);
font-size:10px;
letter-spacing:0.14em;
text-transform:uppercase;
}
.rack{
display:grid;
gap:8px;
margin-top:8px;
}
.rack-row{
height:34px;
display:flex;
align-items:center;
justify-content:space-between;
padding:0 12px;
border:1px solid rgba(92,126,174,0.28);
border-radius:8px;
background:linear-gradient(90deg, rgba(69,240,221,0.1), rgba(98,168,255,0.04));
color:#a9bddb;
font-size:11px;
position:relative;
z-index:1;
}
.leds{display:flex;gap:5px;}
.led{width:7px;height:7px;border-radius:50%;background:var(--green);box-shadow:0 0 10px var(--green);}
.led.warn{background:var(--amber);box-shadow:0 0 10px var(--amber);}
.led.hot{background:var(--red);box-shadow:0 0 10px var(--red);}
.games{
margin-top:30px;
padding-top:28px;
}
.section-head{
display:flex;
align-items:end;
justify-content:space-between;
gap:16px;
margin-bottom:18px;
}
h2{
font-family:'Space Grotesk', sans-serif;
font-size:clamp(30px, 4vw, 48px);
letter-spacing:-0.04em;
color:#fff;
}
.section-copy{
max-width:520px;
color:var(--muted);
line-height:1.6;
font-size:14px;
text-align:right;
}
.game-grid{
display:grid;
grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));
gap:18px;
}
.empty-state{
grid-column:1 / -1;
border:1px dashed rgba(92,126,174,0.4);
border-radius:18px;
padding:28px;
color:var(--muted);
background:rgba(3,7,18,0.28);
line-height:1.7;
}
.game-card{
min-height:310px;
border-radius:22px;
padding:24px;
position:relative;
overflow:hidden;
transition:transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
}
.game-card:hover{
transform:translateY(-4px);
border-color:rgba(69,240,221,0.78);
box-shadow:0 30px 90px rgba(0,0,0,0.52), 0 0 42px rgba(69,240,221,0.16);
}
.game-card::before{
content:"";
position:absolute;
inset:0;
background:
linear-gradient(135deg, rgba(69,240,221,0.15), transparent 34%),
radial-gradient(circle at 88% 22%, rgba(255,189,90,0.17), transparent 16rem);
pointer-events:none;
}
.game-inner{position:relative;z-index:1;}
.game-top{
display:flex;
align-items:center;
justify-content:space-between;
gap:14px;
margin-bottom:36px;
}
.badge{
display:inline-flex;
align-items:center;
gap:8px;
padding:8px 10px;
border:1px solid rgba(69,240,221,0.44);
border-radius:999px;
background:rgba(69,240,221,0.08);
color:#bffcf4;
font-size:11px;
letter-spacing:0.14em;
text-transform:uppercase;
font-weight:800;
}
.badge::before{
content:"";
width:7px;
height:7px;
border-radius:50%;
background:var(--green);
box-shadow:0 0 12px var(--green);
}
.difficulty{color:var(--amber);font-size:12px;font-weight:800;letter-spacing:0.12em;text-transform:uppercase;}
.game-title{
font-family:'Space Grotesk', sans-serif;
color:#fff;
font-size:42px;
line-height:0.95;
letter-spacing:-0.05em;
margin-bottom:14px;
}
.game-desc{
color:#a9bddb;
line-height:1.65;
max-width:620px;
margin-bottom:24px;
}
.tags{
display:flex;
flex-wrap:wrap;
gap:8px;
margin-bottom:26px;
}
.tag{
padding:7px 9px;
border-radius:8px;
background:rgba(3,7,18,0.48);
border:1px solid rgba(92,126,174,0.24);
color:var(--muted);
font-size:11px;
}
.play-link{
display:inline-flex;
align-items:center;
gap:10px;
color:#fff;
font-weight:800;
letter-spacing:0.1em;
text-transform:uppercase;
}
.play-link span{
width:34px;
height:34px;
display:inline-grid;
place-items:center;
border-radius:50%;
border:1px solid rgba(69,240,221,0.58);
color:var(--cyan);
box-shadow:0 0 22px rgba(69,240,221,0.14);
transition:transform 160ms ease;
}
.game-card:hover .play-link span{transform:translateX(3px);}
.footer{
display:flex;
justify-content:space-between;
gap:14px;
flex-wrap:wrap;
margin-top:36px;
color:var(--muted);
font-size:12px;
}
.footer a{
color:#fff;
font-weight:700;
}
code{
color:#c9fff8;
background:rgba(69,240,221,0.08);
border:1px solid rgba(69,240,221,0.18);
padding:2px 6px;
border-radius:6px;
}
@media (max-width:900px){
.hero{grid-template-columns:1fr;}
.section-head{align-items:start;flex-direction:column;}
.section-copy{text-align:left;}
.terminal-body{padding:26px;}
}
@media (max-width:560px){
.shell{width:min(100% - 22px, 1180px);padding-top:18px;}
.topbar{align-items:flex-start;flex-direction:column;margin-bottom:24px;}
.terminal{min-height:auto;border-radius:18px;}
.window-bar{padding:0 12px;font-size:9px;}
.terminal-body{padding:22px 18px;}
.lead{font-size:14px;}
.button{width:100%;}
.metric-grid{grid-template-columns:1fr;}
.game-card{padding:20px;}
.game-title{font-size:34px;}
}
</style>
</head>
<body>
<main class="shell">
<nav class="topbar" aria-label="Site status">
<a class="brand" href="/" aria-label="games.sysmd.uk home">
<span class="brand-mark" aria-hidden="true"></span>
<span>games.sysmd.uk</span>
</a>
<div class="status"><span class="status-dot" aria-hidden="true"></span> Arcade service online</div>
</nav>
<section class="hero" aria-labelledby="page-title">
<div class="terminal">
<div class="window-bar">
<div class="lights" aria-hidden="true"><span class="light"></span><span class="light"></span><span class="light"></span></div>
<span>/srv/sysadmin-arcade</span>
</div>
<div class="terminal-body">
<p class="prompt">root@games:~# ./launch-game</p>
<h1 id="page-title">SysAdmin<br>Arcade<span class="cursor">_</span></h1>
<p class="lead">Even SysAdmins need somewhere to park the pager, stretch the brain, and have a little fun between deploys. Pick a browser game and enjoy a short maintenance window for yourself.</p>
<div class="cta-row">
<a class="button" href="#games">Browse Games</a>
</div>
</div>
</div>
<aside class="side-stack" aria-label="Operations dashboard">
<div class="ops-card">
<div class="ops-label">Arcade Status</div>
<div class="metric-grid">
<div class="metric"><strong>99.99%</strong><span>Arcade uptime</span></div>
<div class="metric"><strong id="game-count">...</strong><span>Games deployed</span></div>
<div class="metric"><strong></strong><span>Fun budget</span></div>
<div class="metric"><strong>100%</strong><span>Free</span></div>
</div>
</div>
<div class="ops-card">
<div class="ops-label">Session Queue</div>
<div class="rack" aria-hidden="true">
<div class="rack-row"><span>coffee</span><span class="leds"><i class="led"></i><i class="led"></i><i class="led"></i></span></div>
<div class="rack-row"><span>break time</span><span class="leds"><i class="led"></i><i class="led"></i><i class="led warn"></i></span></div>
<div class="rack-row"><span>high score</span><span class="leds"><i class="led"></i><i class="led"></i><i class="led"></i></span></div>
<div class="rack-row"><span>pager mode</span><span class="leds"><i class="led"></i><i class="led warn"></i><i class="led"></i></span></div>
</div>
</div>
</aside>
</section>
<section class="games" id="games" aria-labelledby="games-title">
<div class="section-head">
<div>
<div class="section-label">Available Runbooks</div>
<h2 id="games-title">Choose Your Game</h2>
</div>
</div>
<div class="game-grid" id="game-grid" aria-live="polite">
<div class="empty-state">Loading deployed games from <code>games.json</code>...</div>
</div>
</section>
<footer class="footer">
<span>© 2026 <a href="https://www.sysmd.uk/">SysMD</a></span>
<span>Maintained by <a href="https://www.marcodaleo.com/">Marco D'Aleo</a></span>
</footer>
</main>
<script>
const gameGrid = document.querySelector('#game-grid');
const gameCount = document.querySelector('#game-count');
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
"'": '&#39;',
'"': '&quot;'
})[char]);
const renderGame = (game) => {
const tags = Array.isArray(game.tags) ? game.tags : [];
return `
<article class="game-card">
<div class="game-inner">
<div class="game-top">
<span class="badge">${escapeHtml(game.status || 'Online')}</span>
<span class="difficulty">${escapeHtml(game.difficulty || 'Ready')}</span>
</div>
<h3 class="game-title">${escapeHtml(game.name)}</h3>
<p class="game-desc">${escapeHtml(game.description)}</p>
<div class="tags" aria-label="Game tags">
${tags.map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
</div>
<a class="play-link" href="${escapeHtml(game.url)}">${escapeHtml(game.action || 'Play')} <span aria-hidden="true">&gt;</span></a>
</div>
</article>
`;
};
fetch('games.json')
.then((response) => {
if (!response.ok) throw new Error(`Unable to load games.json: ${response.status}`);
return response.json();
})
.then((games) => {
const deployedGames = Array.isArray(games) ? games : [];
gameCount.textContent = deployedGames.length;
gameGrid.innerHTML = deployedGames.length
? deployedGames.map(renderGame).join('')
: '<div class="empty-state">No games are currently listed in <code>games.json</code>.</div>';
})
.catch(() => {
gameCount.textContent = '?';
gameGrid.innerHTML = '<div class="empty-state">Could not load <code>games.json</code>. Serve this directory over HTTP and check the games list path.</div>';
});
</script>
</body>
</html>