Files
homelab/deck/index.html

523 lines
21 KiB
HTML
Raw Permalink 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="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>xiteng.site — Personal Infrastructure</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
<style>
:root {
--bg: #050508;
--surface: #0a0a14;
--card: rgba(255,255,255,0.025);
--card-hover: rgba(255,255,255,0.05);
--text: #e4e4ec;
--text-dim: #8888a0;
--text-muted: #5a5a72;
--accent: #6366f1;
--accent-glow: rgba(99,102,241,0.35);
--green: #22c55e;
--green-glow: rgba(34,197,94,0.3);
--amber: #f59e0b;
--blue: #3b82f6;
--red: #ef4444;
--border: rgba(255,255,255,0.06);
--border-visible: rgba(255,255,255,0.10);
--grid: rgba(255,255,255,0.025);
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth;background:var(--bg);-webkit-text-size-adjust:100%}
body{
font-family:'Inter',system-ui,-apple-system,sans-serif;
background:var(--bg);color:var(--text);
overflow-x:hidden;line-height:1.6;
-webkit-font-smoothing:antialiased;
}
/* ── Canvas Background ── */
#gridCanvas{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:0.6}
/* ── Status Bar ── */
.statusbar{
position:fixed;top:0;left:0;right:0;z-index:100;
height:36px;display:flex;align-items:center;justify-content:space-between;
padding:0 20px;font-family:'JetBrains Mono',monospace;
font-size:11px;color:var(--text-muted);
background:rgba(5,5,8,0.85);
backdrop-filter:blur(12px);
border-bottom:1px solid var(--border);
}
.statusbar a{display:inline-flex;align-items:center;color:var(--text-dim);text-decoration:none;font-weight:600}
.statusbar a:hover{color:var(--text)}
.statusbar .left{display:flex;align-items:center;gap:16px}
.statusbar .dot{
width:7px;height:7px;border-radius:50%;background:var(--green);
box-shadow:0 0 6px var(--green-glow);
margin-right:8px;display:inline-block;vertical-align:middle;
animation:pulse-dot 2s ease-in-out infinite;
}
@keyframes pulse-dot{0%,100%{opacity:1}50%{opacity:0.5}}
.statusbar .right{display:flex;gap:16px}
/* ── Layout ── */
.container{position:relative;z-index:1;max-width:1100px;margin:0 auto;padding:0 24px}
section{padding:100px 0;position:relative}
section:first-of-type{padding-top:160px}
/* ── Typography ── */
.label{
font-family:'JetBrains Mono',monospace;
font-size:12px;font-weight:500;color:var(--accent);
text-transform:uppercase;letter-spacing:0.08em;margin-bottom:16px;display:block;
}
h1{
font-size:clamp(40px,6vw,80px);font-weight:700;
line-height:1.05;letter-spacing:-0.03em;margin-bottom:16px;
}
h1 .hl{color:var(--accent)}
h2{
font-size:clamp(28px,3.5vw,48px);font-weight:600;
line-height:1.15;letter-spacing:-0.02em;margin-bottom:12px;
}
.subtitle{
font-size:clamp(15px,1.4vw,20px);font-weight:300;color:var(--text-dim);
max-width:560px;line-height:1.7;
}
/* ── Hero ── */
.hero-grid{
display:grid;grid-template-columns:repeat(3,1fr);gap:12px;
margin-top:48px;max-width:600px;
}
.hero-stat{
background:var(--card);border:1px solid var(--border);
border-radius:8px;padding:16px 18px;
}
.hero-stat .val{
font-family:'JetBrains Mono',monospace;
font-size:22px;font-weight:700;color:var(--text);
}
.hero-stat .val.up{color:var(--green)}
.hero-stat .lbl{font-size:11px;color:var(--text-muted);margin-top:2px;text-transform:uppercase;letter-spacing:0.05em}
.hero-stat .val .unit{font-size:13px;color:var(--text-dim);font-weight:400}
/* ── Terminal Block ── */
.terminal{
background:rgba(0,0,0,0.5);border:1px solid var(--border-visible);
border-radius:10px;overflow:hidden;font-family:'JetBrains Mono',monospace;
font-size:clamp(11px,0.8vw,13px);line-height:1.8;
}
.terminal .bar{
height:28px;background:rgba(255,255,255,0.03);
display:flex;align-items:center;padding:0 12px;gap:6px;
border-bottom:1px solid var(--border);
}
.terminal .bar .tb{width:10px;height:10px;border-radius:50%}
.terminal .bar .tb.r{background:var(--red)}
.terminal .bar .tb.y{background:var(--amber)}
.terminal .bar .tb.g{background:var(--green)}
.terminal .bar .ttl{font-size:11px;color:var(--text-muted);margin-left:8px}
.terminal .body{padding:16px 18px;color:var(--text-dim)}
.terminal .body .prompt{color:var(--green)}
.terminal .body .cmd{color:var(--text)}
.terminal .body .out{color:var(--text-dim)}
.terminal .body .cursor{
display:inline-block;width:8px;height:15px;background:var(--accent);
vertical-align:text-bottom;margin-left:1px;
animation:blink 1s step-end infinite;
}
@keyframes blink{50%{opacity:0}}
/* ── Architecture Diagram ── */
.arch-diagram{
display:flex;align-items:center;gap:0;flex-wrap:wrap;
justify-content:center;padding:20px 0;
}
.arch-node{
background:var(--surface);border:1px solid var(--border-visible);
border-radius:12px;padding:20px 28px;text-align:center;
transition:all 0.3s;position:relative;
}
.arch-node:hover{
border-color:var(--accent);
box-shadow:0 0 24px var(--accent-glow);
transform:translateY(-2px);
}
.arch-node .n{font-size:18px;font-weight:600;color:var(--text)}
.arch-node .d{font-size:11px;color:var(--text-muted);margin-top:3px;font-family:'JetBrains Mono',monospace}
.arch-node.accent{border-color:var(--accent);background:rgba(99,102,241,0.04)}
.arch-node.accent .n{color:var(--accent)}
.arch-arrow{color:var(--text-muted);font-size:18px;padding:0 10px;font-family:'JetBrains Mono',monospace;flex-shrink:0}
.arch-pulse{animation:arch-glow 2.5s ease-in-out infinite}
.arch-pulse:nth-child(2){animation-delay:0.5s}
.arch-pulse:nth-child(3){animation-delay:1s}
@keyframes arch-glow{0%,100%{box-shadow:0 0 0 rgba(99,102,241,0)}50%{box-shadow:0 0 20px var(--accent-glow)}}
/* ── Service Cards ── */
.card-grid{
display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));
gap:16px;margin-top:40px;
}
.svc-card{
background:var(--card);border:1px solid var(--border);
border-radius:12px;padding:28px;cursor:pointer;
transition:all 0.3s;position:relative;overflow:hidden;
}
.svc-card::before{
content:'';position:absolute;inset:0;border-radius:12px;
background:radial-gradient(600px circle at var(--mx,50%) var(--my,50%),rgba(99,102,241,0.06),transparent 40%);
opacity:0;transition:opacity 0.3s;
}
.svc-card:hover::before{opacity:1}
.svc-card:hover{
border-color:var(--border-visible);
background:var(--card-hover);
transform:translateY(-2px);
}
.svc-card .indicator{
width:8px;height:8px;border-radius:50%;background:var(--green);
box-shadow:0 0 8px var(--green-glow);
display:inline-block;margin-bottom:16px;
animation:pulse-dot 3s ease-in-out infinite;
}
.svc-card .indicator:nth-child(odd){animation-delay:1.5s}
.svc-card h3{font-size:20px;font-weight:600;margin-bottom:4px;letter-spacing:-0.01em}
.svc-card .url{
font-family:'JetBrains Mono',monospace;font-size:11px;
color:var(--text-muted);margin-bottom:12px;
}
.svc-card p{font-size:13px;color:var(--text-dim);line-height:1.6}
.svc-card .tags{margin-top:14px;display:flex;gap:6px;flex-wrap:wrap}
.svc-card .tag{
font-size:10px;font-weight:600;text-transform:uppercase;
letter-spacing:0.04em;padding:3px 8px;border-radius:4px;
color:var(--accent);background:rgba(99,102,241,0.08);
border:1px solid rgba(99,102,241,0.12);
}
/* ── Stack Pills ── */
.stack-section{display:flex;flex-direction:column;gap:20px;margin-top:40px}
.stack-row{display:flex;align-items:flex-start;gap:16px;flex-wrap:wrap}
.stack-label{
font-family:'JetBrains Mono',monospace;font-size:12px;font-weight:700;
color:var(--green);min-width:80px;text-align:right;padding-top:5px;
}
.stack-pills{display:flex;gap:8px;flex-wrap:wrap}
.stack-pill{
padding:6px 14px;border-radius:20px;font-size:13px;font-weight:500;
background:var(--card);border:1px solid var(--border);
color:var(--text-dim);transition:all 0.2s;
}
.stack-pill:hover{border-color:var(--border-visible);color:var(--text)}
.stack-pill.hl{border-color:var(--accent);color:var(--accent);background:rgba(99,102,241,0.06)}
/* ── CTA / Command Prompt ── */
.cmd-prompt{
background:var(--surface);border:1px solid var(--border-visible);
border-radius:12px;padding:20px 24px;display:flex;align-items:center;
gap:12px;font-family:'JetBrains Mono',monospace;font-size:15px;
max-width:560px;margin:0 auto;margin-top:48px;transition:all 0.3s;
}
.cmd-prompt:focus-within{border-color:var(--accent);box-shadow:0 0 20px var(--accent-glow)}
.cmd-prompt .pr{color:var(--green);flex-shrink:0}
.cmd-prompt input{
flex:1;background:none;border:none;outline:none;
color:var(--text);font:inherit;caret-color:var(--accent);
}
.cmd-prompt input::placeholder{color:var(--text-muted)}
/* ── Footer ── */
footer{
text-align:center;padding:60px 24px;border-top:1px solid var(--border);
font-family:'JetBrains Mono',monospace;font-size:12px;color:var(--text-muted);
}
footer a{color:var(--accent);text-decoration:none}
footer a:hover{text-decoration:underline}
/* ── Scroll Reveal ── */
.reveal{opacity:0;transform:translateY(30px);transition:all 0.7s cubic-bezier(0.16,1,0.3,1)}
.reveal.visible{opacity:1;transform:translateY(0)}
/* ── Responsive ── */
@media(max-width:768px){
section{padding:60px 0}
section:first-of-type{padding-top:120px}
.arch-diagram{flex-direction:column;gap:4px}
.arch-arrow{display:none}
.hero-grid{grid-template-columns:repeat(2,1fr)}
.stack-row{flex-direction:column;gap:6px}
.stack-label{text-align:left;min-width:auto}
.card-grid{grid-template-columns:1fr}
.statusbar .right{display:none}
}
@media(max-width:400px){
.hero-grid{grid-template-columns:1fr 1fr;gap:6px}
.hero-stat{padding:10px 12px}
.hero-stat .val{font-size:18px}
}
</style>
</head>
<body>
<canvas id="gridCanvas"></canvas>
<!-- ═══ Status Bar ═══ -->
<header class="statusbar">
<div class="left"><a href="https://xiteng.site/?focus=deck#services">← xiteng.site</a><span><span class="dot"></span> all systems operational</span></div>
<div class="right">UTC+8 · Arch Linux · Docker · <span style="color:var(--green)">UP 42d</span></div>
</header>
<main class="container">
<!-- ═══ Hero ═══ -->
<section>
<div class="reveal">
<span class="label">// Personal Infrastructure</span>
<h1><span class="hl">xiteng</span>.site</h1>
<p class="subtitle">
自建开发基础设施。一台 Linux 机器,Docker Compose 驱动,
Cloudflare Tunnel 接入,Authentik 统一认证。
</p>
</div>
<div class="reveal hero-grid">
<div class="hero-stat"><div class="val up">8</div><div class="lbl">Services</div></div>
<div class="hero-stat"><div class="val">1<span class="unit"> host</span></div><div class="lbl">Arch Linux</div></div>
<div class="hero-stat"><div class="val up">99.9<span class="unit">%</span></div><div class="lbl">Uptime (30d)</div></div>
</div>
<div class="reveal" style="margin-top:40px">
<div class="terminal">
<div class="bar"><div class="tb r"></div><div class="tb y"></div><div class="tb g"></div><span class="ttl">ssh aliyun — bash — 80×24</span></div>
<div class="body">
<span class="prompt">$</span> <span class="cmd">docker compose ps</span><br>
<span class="out">NAME &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;STATUS<br>
gitea &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Up 7 days<br>
hedgedoc &nbsp;&nbsp;&nbsp;&nbsp;Up 7 days<br>
seaweedfs &nbsp;&nbsp;&nbsp;Up 7 days<br>
uptime-kuma &nbsp;Up 7 days<br>
homepage &nbsp;&nbsp;&nbsp;&nbsp;Up 7 days<br>
authentik &nbsp;&nbsp;&nbsp;Up 7 days<br>
deck &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;Up 5 minutes<br>
cloudflared &nbsp;Up 7 days</span><br>
<span class="prompt">$</span> <span class="cursor"></span>
</div>
</div>
</div>
</section>
<!-- ═══ Architecture ═══ -->
<section>
<div class="reveal"><span class="label">// How It Works</span></div>
<div class="reveal"><h2>Traffic Flow</h2></div>
<div class="reveal arch-diagram">
<div class="arch-node"><div class="n">Internet</div><div class="d">HTTPS</div></div>
<div class="arch-arrow"></div>
<div class="arch-node accent arch-pulse"><div class="n">Cloudflare</div><div class="d">Tunnel · CDN</div></div>
<div class="arch-arrow"></div>
<div class="arch-node arch-pulse"><div class="n">Traefik</div><div class="d">TLS · Router</div></div>
<div class="arch-arrow"></div>
<div class="arch-node accent arch-pulse"><div class="n">Authentik</div><div class="d">SSO · Auth</div></div>
<div class="arch-arrow"></div>
<div class="arch-node"><div class="n">Services</div><div class="d">8 containers</div></div>
</div>
</section>
<!-- ═══ Services ═══ -->
<section>
<div class="reveal"><span class="label">// Services</span></div>
<div class="reveal"><h2>What's Running</h2></div>
<div class="reveal card-grid">
<a href="https://git.xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>Gitea</h3>
<div class="url">git.xiteng.site</div>
<p>自建 Git 托管。Pull Request、CI/CD Actions、包仓库、OAuth2 登录。</p>
<div class="tags"><span class="tag">git</span><span class="tag">ci/cd</span><span class="tag">oauth2</span></div>
</a>
<a href="https://notes.xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>HedgeDoc</h3>
<div class="url">notes.xiteng.site</div>
<p>实时协作 Markdown。Mermaid 图表、MathJax 公式、OIDC 登录。</p>
<div class="tags"><span class="tag">docs</span><span class="tag">collab</span><span class="tag">oidc</span></div>
</a>
<a href="https://s3.xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>SeaweedFS</h3>
<div class="url">s3.xiteng.site</div>
<p>S3 兼容对象存储。60MB 镜像,替代 MinIO。Filer UI + REST API。</p>
<div class="tags"><span class="tag">s3</span><span class="tag">storage</span></div>
</a>
<a href="https://status.xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>Uptime Kuma</h3>
<div class="url">status.xiteng.site</div>
<p>健康监控。60s 间隔探测,飞书/邮件告警,公开状态页。</p>
<div class="tags"><span class="tag">monitor</span><span class="tag">alerts</span></div>
</a>
<a href="https://xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>Homepage</h3>
<div class="url">xiteng.site</div>
<p>统一仪表盘。Docker 自动发现,书签 & 搜索,OIDC 登录。</p>
<div class="tags"><span class="tag">dashboard</span><span class="tag">oidc</span></div>
</a>
<a href="https://auth.xiteng.site" class="svc-card" style="text-decoration:none;color:inherit">
<div class="indicator"></div>
<h3>Authentik</h3>
<div class="url">auth.xiteng.site</div>
<p>身份认证平台。OAuth2/OIDC ProviderForwardAuth 代理保护。</p>
<div class="tags"><span class="tag">sso</span><span class="tag">security</span></div>
</a>
</div>
</section>
<!-- ═══ Stack ═══ -->
<section>
<div class="reveal"><span class="label">// Stack</span></div>
<div class="reveal"><h2>Technology</h2></div>
<div class="reveal stack-section">
<div class="stack-row">
<div class="stack-label">Infra</div>
<div class="stack-pills">
<span class="stack-pill hl">Docker Compose</span>
<span class="stack-pill">Arch Linux</span>
<span class="stack-pill">Cloudflare Tunnel</span>
<span class="stack-pill">frp Transit</span>
</div>
</div>
<div class="stack-row">
<div class="stack-label">Proxy</div>
<div class="stack-pills">
<span class="stack-pill hl">Traefik v3</span>
<span class="stack-pill">Let's Encrypt</span>
<span class="stack-pill">Cloudflare DNS</span>
</div>
</div>
<div class="stack-row">
<div class="stack-label">Data</div>
<div class="stack-pills">
<span class="stack-pill hl">PostgreSQL</span>
<span class="stack-pill">Redis</span>
<span class="stack-pill">SeaweedFS</span>
</div>
</div>
<div class="stack-row">
<div class="stack-label">Connect</div>
<div class="stack-pills">
<span class="stack-pill hl">Authentik</span>
<span class="stack-pill">Hermes Agent</span>
<span class="stack-pill">Feishu Bot</span>
</div>
</div>
</div>
</section>
<!-- ═══ CTA ═══ -->
<section style="text-align:center">
<div class="reveal">
<span class="label">// Try It</span>
<h2>Open a Terminal</h2>
<p class="subtitle" style="margin:0 auto 0 auto">访问任意服务,Authentik 统一认证。或输入命令探索。</p>
</div>
<div class="reveal cmd-prompt">
<span class="pr">$</span>
<input type="text" id="cmdInput" placeholder="输入 help 查看可用命令…" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">
</div>
</section>
</main>
<footer>
<a href="https://git.xiteng.site">git.xiteng.site</a> &nbsp;·&nbsp;
<a href="https://notes.xiteng.site">notes</a> &nbsp;·&nbsp;
<a href="https://status.xiteng.site">status</a> &nbsp;·&nbsp;
<a href="https://xiteng.site">home</a> &nbsp;·&nbsp;
Built with Hermes Agent
</footer>
<script>
// ═══ Grid Canvas ═══
(function(){
const c=document.getElementById('gridCanvas'),x=c.getContext('2d');
let W,H,mx=-100,my=-100;
function resize(){W=c.width=window.innerWidth;H=c.height=window.innerHeight}
resize();window.addEventListener('resize',resize);
document.addEventListener('mousemove',e=>{mx=e.clientX;my=e.clientY});
const dots=[];
for(let i=0;i<80;i++)dots.push({x:Math.random()*2000,y:Math.random()*2000,r:1+Math.random()*1.5,vx:(Math.random()-0.5)*0.3,vy:(Math.random()-0.5)*0.3});
function draw(){
x.clearRect(0,0,W,H);
// Grid
x.strokeStyle='rgba(255,255,255,0.018)';x.lineWidth=0.5;
const s=50;
for(let gx=s;gx<W;gx+=s){x.beginPath();x.moveTo(gx,0);x.lineTo(gx,H);x.stroke()}
for(let gy=s;gy<H;gy+=s){x.beginPath();x.moveTo(0,gy);x.lineTo(W,gy);x.stroke()}
// Dots
dots.forEach(d=>{
d.x+=d.vx;d.y+=d.vy;
if(d.x<0)d.x=2000;if(d.x>2000)d.x=0;
if(d.y<0)d.y=2000;if(d.y>2000)d.y=0;
const dx=d.x-mx,dy=d.y-my,dist=Math.sqrt(dx*dx+dy*dy);
const glow=Math.max(0,1-dist/200);
const r=d.r+glow*2;
x.beginPath();x.arc(d.x,d.y,r,0,Math.PI*2);
const a=0.15+glow*0.4;
x.fillStyle=`rgba(99,102,241,${a})`;x.fill();
});
requestAnimationFrame(draw);
}
draw();
})();
// ═══ Scroll Reveal ═══
(function(){
const obs=new IntersectionObserver(entries=>{
entries.forEach(e=>{if(e.isIntersecting)e.target.classList.add('visible')});
},{threshold:0.15});
document.querySelectorAll('.reveal').forEach(el=>obs.observe(el));
})();
// ═══ Card Glow Follow ═══
document.querySelectorAll('.svc-card').forEach(card=>{
card.addEventListener('mousemove',e=>{
const r=card.getBoundingClientRect();
card.style.setProperty('--mx',(e.clientX-r.left)+'px');
card.style.setProperty('--my',(e.clientY-r.top)+'px');
});
});
// ═══ Command Prompt ═══
(function(){
const input=document.getElementById('cmdInput');
const routes={
'git':'https://git.xiteng.site',
'notes':'https://notes.xiteng.site',
's3':'https://s3.xiteng.site',
'status':'https://status.xiteng.site',
'home':'https://xiteng.site',
'auth':'https://auth.xiteng.site',
'deck':'https://deck.xiteng.site',
'file':'https://file.xiteng.site',
};
input.addEventListener('keydown',e=>{
if(e.key!=='Enter')return;
const v=input.value.trim().toLowerCase();
input.value='';
if(v==='help'||v==='?'){
let msg='Available: ';
Object.keys(routes).forEach(k=>msg+=k+' ');
msg+='\nType a service name to navigate, or "help" to see this.';
alert(msg);
}else if(routes[v]){
window.open(routes[v],'_blank');
}else if(v){
alert(`Unknown command: ${v}\nType "help" to see available commands.`);
}
});
})();
</script>
</body>
</html>