fix(ssh): 修复 IPv6 地址连接失败和重连计数器不递增的问题

- IPv6 地址传入 connect() API 前自动添加方括号包裹,避免冒号被误解析为 host:port 分隔符
- 移除 WebSocket onopen 中的 reconnectAttempts 重置,改为 SSH 认证成功时才重置,修复重连计数始终显示 1/5 的问题
This commit is contained in:
TanXin committed 2026-06-21 17:23:51 +08:00
1 parent 4c4476cf5e
commit 0871b64426
3 files changed
+248 -248

No files matched your search

+1 -2
View File
@@ -131,7 +131,6 @@ export class SSHTerminal {
privateKey: config.privateKey,
}));
this.reconnectAttempts = 0;
this.startHeartbeat();
resolve();
};
@@ -160,7 +159,6 @@ export class SSHTerminal {
ws.onopen = () => {
this.terminal.writeln('\x1b[32m[+] WebSocket connected, authenticating...\x1b[0m');
this.reconnectAttempts = 0;
this.startHeartbeat();
};
@@ -188,6 +186,7 @@ export class SSHTerminal {
case 'status':
this.terminal.writeln(`\x1b[32m[*] ${msg.message}\x1b[0m`);
if (msg.message === '认证成功') {
this.reconnectAttempts = 0;
const statusText = document.getElementById('status-text');
if (statusText) statusText.innerHTML = '<span class="w-2 h-2 bg-[#4af626] inline-block animate-pulse"></span> STATUS: ONLINE';
}
+2 -1
View File
@@ -169,7 +169,8 @@ export class SSHSessionDO {
}
const { connect } = await import('cloudflare:sockets');
const socket = connect({ hostname: config.host, port: config.port });
const hostname = config.host.includes(':') ? `[${config.host}]` : config.host;
const socket = connect({ hostname, port: config.port });
await socket.opened;
+245 -245
View File
@@ -1,46 +1,46 @@
// Automatically generated by scripts/build-html.js. Do not edit directly.
export const HTML = `<!DOCTYPE html>
<html class="dark" lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CloudSSH</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"background": "#131313",
"on-surface": "#e5e2e1",
"on-surface-variant": "#bbccb0",
"outline": "#86957d",
"outline-variant": "#3c4b36",
"primary-container": "#4af626",
"on-primary-fixed": "#022100",
"surface": "#131313",
"surface-variant": "#353534",
"secondary-container": "#14d1ff",
"secondary-fixed": "#b7eaff",
"error": "#ffb4ab",
"error-container": "#93000a",
"surface-container-lowest": "#0e0e0e"
},
fontFamily: {
"body": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"label": ["JetBrains Mono"],
"code": ["JetBrains Mono"]
}
}
}
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
export const HTML = `<!DOCTYPE html>
<html class="dark" lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>CloudSSH</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
<script>
tailwind.config = {
darkMode: "class",
theme: {
extend: {
colors: {
"background": "#131313",
"on-surface": "#e5e2e1",
"on-surface-variant": "#bbccb0",
"outline": "#86957d",
"outline-variant": "#3c4b36",
"primary-container": "#4af626",
"on-primary-fixed": "#022100",
"surface": "#131313",
"surface-variant": "#353534",
"secondary-container": "#14d1ff",
"secondary-fixed": "#b7eaff",
"error": "#ffb4ab",
"error-container": "#93000a",
"surface-container-lowest": "#0e0e0e"
},
fontFamily: {
"body": ["JetBrains Mono"],
"headline": ["JetBrains Mono"],
"label": ["JetBrains Mono"],
"code": ["JetBrains Mono"]
}
}
}
}
</script>
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script type="module">var Ws=Object.defineProperty;var $s=(r,e,_)=>e in r?Ws(r,e,{enumerable:!0,configurable:!0,writable:!0,value:_}):r[e]=_;var Y=(r,e,_)=>$s(r,typeof e!="symbol"?e+"":e,_);(function(){const e=document.createElement("link").relList;if(e&&e.supports&&e.supports("modulepreload"))return;for(const g of document.querySelectorAll('link[rel="modulepreload"]'))C(g);new MutationObserver(g=>{for(const k of g)if(k.type==="childList")for(const w of k.addedNodes)w.tagName==="LINK"&&w.rel==="modulepreload"&&C(w)}).observe(document,{childList:!0,subtree:!0});function _(g){const k={};return g.integrity&&(k.integrity=g.integrity),g.referrerPolicy&&(k.referrerPolicy=g.referrerPolicy),g.crossOrigin==="use-credentials"?k.credentials="include":g.crossOrigin==="anonymous"?k.credentials="omit":k.credentials="same-origin",k}function C(g){if(g.ep)return;g.ep=!0;const k=_(g);fetch(g.href,k)}})();var wt={exports:{}},di;function Us(){return di||(di=1,(function(r,e){(function(_,C){r.exports=C()})(globalThis,(()=>(()=>{var _={4567:function(w,s,a){var l=this&&this.__decorate||function(i,n,c,m){var S,y=arguments.length,f=y<3?n:m===null?m=Object.getOwnPropertyDescriptor(n,c):m;if(typeof Reflect=="object"&&typeof Reflect.decorate=="function")f=Reflect.decorate(i,n,c,m);else for(var L=i.length-1;L>=0;L--)(S=i[L])&&(f=(y<3?S(f):y>3?S(n,c,f):S(n,c))||f);return y>3&&f&&Object.defineProperty(n,c,f),f},d=this&&this.__param||function(i,n){return function(c,m){n(c,m,i)}};Object.defineProperty(s,"__esModule",{value:!0}),s.AccessibilityManager=void 0;const h=a(9042),u=a(9924),v=a(844),b=a(4725),p=a(2585),t=a(3656);let o=s.AccessibilityManager=class extends v.Disposable{constructor(i,n,c,m){super(),this._terminal=i,this._coreBrowserService=c,this._renderService=m,this._rowColumns=new WeakMap,this._liveRegionLineCount=0,this._charsToConsume=[],this._charsToAnnounce="",this._accessibilityContainer=this._coreBrowserService.mainDocument.createElement("div"),this._accessibilityContainer.classList.add("xterm-accessibility"),this._rowContainer=this._coreBrowserService.mainDocument.createElement("div"),this._rowContainer.setAttribute("role","list"),this._rowContainer.classList.add("xterm-accessibility-tree"),this._rowElements=[];for(let S=0;S<this._terminal.rows;S++)this._rowElements[S]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[S]);if(this._topBoundaryFocusListener=S=>this._handleBoundaryFocus(S,0),this._bottomBoundaryFocusListener=S=>this._handleBoundaryFocus(S,1),this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions(),this._accessibilityContainer.appendChild(this._rowContainer),this._liveRegion=this._coreBrowserService.mainDocument.createElement("div"),this._liveRegion.classList.add("live-region"),this._liveRegion.setAttribute("aria-live","assertive"),this._accessibilityContainer.appendChild(this._liveRegion),this._liveRegionDebouncer=this.register(new u.TimeBasedDebouncer(this._renderRows.bind(this))),!this._terminal.element)throw new Error("Cannot enable accessibility before Terminal.open");this._terminal.element.insertAdjacentElement("afterbegin",this._accessibilityContainer),this.register(this._terminal.onResize((S=>this._handleResize(S.rows)))),this.register(this._terminal.onRender((S=>this._refreshRows(S.start,S.end)))),this.register(this._terminal.onScroll((()=>this._refreshRows()))),this.register(this._terminal.onA11yChar((S=>this._handleChar(S)))),this.register(this._terminal.onLineFeed((()=>this._handleChar(\`
\`)))),this.register(this._terminal.onA11yTab((S=>this._handleTab(S)))),this.register(this._terminal.onKey((S=>this._handleKey(S.key)))),this.register(this._terminal.onBlur((()=>this._clearLiveRegion()))),this.register(this._renderService.onDimensionsChange((()=>this._refreshRowsDimensions()))),this.register((0,t.addDisposableDomListener)(document,"selectionchange",(()=>this._handleSelectionChange()))),this.register(this._coreBrowserService.onDprChange((()=>this._refreshRowsDimensions()))),this._refreshRows(),this.register((0,v.toDisposable)((()=>{this._accessibilityContainer.remove(),this._rowElements.length=0})))}_handleTab(i){for(let n=0;n<i;n++)this._handleChar(" ")}_handleChar(i){this._liveRegionLineCount<21&&(this._charsToConsume.length>0?this._charsToConsume.shift()!==i&&(this._charsToAnnounce+=i):this._charsToAnnounce+=i,i===\`
\`&&(this._liveRegionLineCount++,this._liveRegionLineCount===21&&(this._liveRegion.textContent+=h.tooMuchOutput)))}_clearLiveRegion(){this._liveRegion.textContent="",this._liveRegionLineCount=0}_handleKey(i){this._clearLiveRegion(),new RegExp("\\\\p{Control}","u").test(i)||this._charsToConsume.push(i)}_refreshRows(i,n){this._liveRegionDebouncer.refresh(i,n,this._terminal.rows)}_renderRows(i,n){const c=this._terminal.buffer,m=c.lines.length.toString();for(let S=i;S<=n;S++){const y=c.lines.get(c.ydisp+S),f=[],L=(y==null?void 0:y.translateToString(!0,void 0,void 0,f))||"",M=(c.ydisp+S+1).toString(),T=this._rowElements[S];T&&(L.length===0?(T.innerText=" ",this._rowColumns.set(T,[0,1])):(T.textContent=L,this._rowColumns.set(T,f)),T.setAttribute("aria-posinset",M),T.setAttribute("aria-setsize",m))}this._announceCharacters()}_announceCharacters(){this._charsToAnnounce.length!==0&&(this._liveRegion.textContent+=this._charsToAnnounce,this._charsToAnnounce="")}_handleBoundaryFocus(i,n){const c=i.target,m=this._rowElements[n===0?1:this._rowElements.length-2];if(c.getAttribute("aria-posinset")===(n===0?"1":\`\${this._terminal.buffer.lines.length}\`)||i.relatedTarget!==m)return;let S,y;if(n===0?(S=c,y=this._rowElements.pop(),this._rowContainer.removeChild(y)):(S=this._rowElements.shift(),y=c,this._rowContainer.removeChild(S)),S.removeEventListener("focus",this._topBoundaryFocusListener),y.removeEventListener("focus",this._bottomBoundaryFocusListener),n===0){const f=this._createAccessibilityTreeNode();this._rowElements.unshift(f),this._rowContainer.insertAdjacentElement("afterbegin",f)}else{const f=this._createAccessibilityTreeNode();this._rowElements.push(f),this._rowContainer.appendChild(f)}this._rowElements[0].addEventListener("focus",this._topBoundaryFocusListener),this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._terminal.scrollLines(n===0?-1:1),this._rowElements[n===0?1:this._rowElements.length-2].focus(),i.preventDefault(),i.stopImmediatePropagation()}_handleSelectionChange(){var L;if(this._rowElements.length===0)return;const i=document.getSelection();if(!i)return;if(i.isCollapsed)return void(this._rowContainer.contains(i.anchorNode)&&this._terminal.clearSelection());if(!i.anchorNode||!i.focusNode)return void console.error("anchorNode and/or focusNode are null");let n={node:i.anchorNode,offset:i.anchorOffset},c={node:i.focusNode,offset:i.focusOffset};if((n.node.compareDocumentPosition(c.node)&Node.DOCUMENT_POSITION_PRECEDING||n.node===c.node&&n.offset>c.offset)&&([n,c]=[c,n]),n.node.compareDocumentPosition(this._rowElements[0])&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_FOLLOWING)&&(n={node:this._rowElements[0].childNodes[0],offset:0}),!this._rowContainer.contains(n.node))return;const m=this._rowElements.slice(-1)[0];if(c.node.compareDocumentPosition(m)&(Node.DOCUMENT_POSITION_CONTAINED_BY|Node.DOCUMENT_POSITION_PRECEDING)&&(c={node:m,offset:((L=m.textContent)==null?void 0:L.length)??0}),!this._rowContainer.contains(c.node))return;const S=({node:M,offset:T})=>{const B=M instanceof Text?M.parentNode:M;let R=parseInt(B==null?void 0:B.getAttribute("aria-posinset"),10)-1;if(isNaN(R))return console.warn("row is invalid. Race condition?"),null;const O=this._rowColumns.get(B);if(!O)return console.warn("columns is null. Race condition?"),null;let F=T<O.length?O[T]:O.slice(-1)[0]+1;return F>=this._terminal.cols&&(++R,F=0),{row:R,column:F}},y=S(n),f=S(c);if(y&&f){if(y.row>f.row||y.row===f.row&&y.column>=f.column)throw new Error("invalid range");this._terminal.select(y.column,y.row,(f.row-y.row)*this._terminal.cols-y.column+f.column)}}_handleResize(i){this._rowElements[this._rowElements.length-1].removeEventListener("focus",this._bottomBoundaryFocusListener);for(let n=this._rowContainer.children.length;n<this._terminal.rows;n++)this._rowElements[n]=this._createAccessibilityTreeNode(),this._rowContainer.appendChild(this._rowElements[n]);for(;this._rowElements.length>i;)this._rowContainer.removeChild(this._rowElements.pop());this._rowElements[this._rowElements.length-1].addEventListener("focus",this._bottomBoundaryFocusListener),this._refreshRowsDimensions()}_createAccessibilityTreeNode(){const i=this._coreBrowserService.mainDocument.createElement("div");return i.setAttribute("role","listitem"),i.tabIndex=-1,this._refreshRowDimensions(i),i}_refreshRowsDimensions(){if(this._renderService.dimensions.css.cell.height){this._accessibilityContainer.style.width=\`\${this._renderService.dimensions.css.canvas.width}px\`,this._rowElements.length!==this._terminal.rows&&this._handleResize(this._terminal.rows);for(let i=0;i<this._terminal.rows;i++)this._refreshRowDimensions(this._rowElements[i])}}_refreshRowDimensions(i){i.style.height=\`\${this._renderService.dimensions.css.cell.height}px\`}};s.AccessibilityManager=o=l([d(1,p.IInstantiationService),d(2,b.ICoreBrowserService),d(3,b.IRenderService)],o)},3614:(w,s)=>{function a(u){return u.replace(/\\r?\\n/g,"\\Line truncated
@@ -285,207 +285,207 @@ void main() {
* other features.
*/.xterm{cursor:text;position:relative;user-select:none;-ms-user-select:none;-webkit-user-select:none}.xterm.focus,.xterm:focus{outline:none}.xterm .xterm-helpers{position:absolute;top:0;z-index:5}.xterm .xterm-helper-textarea{padding:0;border:0;margin:0;position:absolute;opacity:0;left:-9999em;top:0;width:0;height:0;z-index:-5;white-space:nowrap;overflow:hidden;resize:none}.xterm .composition-view{background:#000;color:#fff;display:none;position:absolute;white-space:nowrap;z-index:1}.xterm .composition-view.active{display:block}.xterm .xterm-viewport{background-color:#000;overflow-y:scroll;cursor:default;position:absolute;right:0;left:0;top:0;bottom:0}.xterm .xterm-screen{position:relative}.xterm .xterm-screen canvas{position:absolute;left:0;top:0}.xterm .xterm-scroll-area{visibility:hidden}.xterm-char-measure-element{display:inline-block;visibility:hidden;position:absolute;top:0;left:-9999em;line-height:normal}.xterm.enable-mouse-events{cursor:default}.xterm.xterm-cursor-pointer,.xterm .xterm-cursor-pointer{cursor:pointer}.xterm.column-select.focus{cursor:crosshair}.xterm .xterm-accessibility:not(.debug),.xterm .xterm-message{position:absolute;left:0;top:0;bottom:0;right:0;z-index:10;color:transparent;pointer-events:none}.xterm .xterm-accessibility-tree:not(.debug) *::selection{color:transparent}.xterm .xterm-accessibility-tree{-webkit-user-select:text;user-select:text;white-space:pre}.xterm .live-region{position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden}.xterm-dim{opacity:1!important}.xterm-underline-1{text-decoration:underline}.xterm-underline-2{text-decoration:double underline}.xterm-underline-3{text-decoration:wavy underline}.xterm-underline-4{text-decoration:dotted underline}.xterm-underline-5{text-decoration:dashed underline}.xterm-overline{text-decoration:overline}.xterm-overline.xterm-underline-1{text-decoration:overline underline}.xterm-overline.xterm-underline-2{text-decoration:overline double underline}.xterm-overline.xterm-underline-3{text-decoration:overline wavy underline}.xterm-overline.xterm-underline-4{text-decoration:overline dotted underline}.xterm-overline.xterm-underline-5{text-decoration:overline dashed underline}.xterm-strikethrough{text-decoration:line-through}.xterm-screen .xterm-decoration-container .xterm-decoration{z-index:6;position:absolute}.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer{z-index:7}.xterm-decoration-overview-ruler{z-index:8;position:absolute;top:0;right:0;pointer-events:none}.xterm-decoration-top{z-index:2;position:relative}
</style>
</head>
<body class="min-h-screen font-body text-sm overflow-hidden">
<div id="app">
<!-- Login Page (匿名连接) -->
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
<div class="scanlines"></div>
<div class="flicker"></div>
<main class="w-full max-w-md relative z-10">
<div class="mb-8 text-center">
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
</div>
<div class="cyber-box p-6 shadow-2xl relative">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
</div>
<div id="connection-form-container"></div>
</div>
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
<div class="mt-4 text-center">
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
</div>
</main>
</div>
<!-- User Space Page (登录后) -->
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
</div>
<div class="flex items-center gap-4">
<div id="user-info" class="flex items-center gap-3">
<!-- 动态渲染:头像 + 用户名 -->
</div>
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
</button>
</div>
</header>
<!-- Server List Area -->
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
<div class="scanlines pointer-events-none"></div>
<div class="max-w-5xl mx-auto">
<!-- 顶部操作栏 -->
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
</div>
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_SERVER
</button>
</div>
<!-- 服务器卡片网格 -->
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 由 JS 动态渲染 -->
</div>
<!-- 空状态 -->
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_FIRST_SERVER
</button>
</div>
</div>
</main>
<!-- Footer -->
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
<!-- Server Modal (添加/编辑) -->
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
</button>
</div>
<form id="server-form" class="space-y-4">
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="col-span-3">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">&gt;</span>
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
</div>
</div>
<div class="col-span-1">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">:</span>
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
</div>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
<div class="flex gap-2 mb-3">
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
</div>
<div id="modal-password-section">
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
</div>
</div>
<div id="modal-key-section" style="display:none;">
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...粘贴私钥内容...&#10;-----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
</div>
</div>
<div class="pt-2">
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
SAVE_SERVER
</button>
</div>
</form>
</div>
</div>
<!-- Terminal Page -->
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
</div>
<div class="flex items-center gap-4 text-primary-container">
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
<option value="cyberpunk">Cyberpunk</option>
<option value="glacier">Glacier</option>
<option value="gruvbox">Gruvbox</option>
</select>
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
</button>
</div>
</header>
<!-- Main Terminal -->
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
<!-- Status Bar -->
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
<div class="flex gap-6">
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
<span id="term-host">Host: -</span>
<span id="term-user">User: -</span>
<span id="term-port">Port: -</span>
</div>
<div>
<span id="term-info"></span>
</div>
</div>
<!-- Terminal Output -->
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
</div>
</main>
<!-- Footer -->
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
</div>
</body>
</html>
</head>
<body class="min-h-screen font-body text-sm overflow-hidden">
<div id="app">
<!-- Login Page (匿名连接) -->
<div id="auth-section" class="min-h-screen flex items-center justify-center p-6 relative">
<div class="scanlines"></div>
<div class="flicker"></div>
<main class="w-full max-w-md relative z-10">
<div class="mb-8 text-center">
<div class="text-3xl font-bold text-[#4af626] tracking-tighter mb-2">CloudSSH<span class="blinking-cursor"></span></div>
</div>
<div class="cyber-box p-6 shadow-2xl relative">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#4af626] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-8 pb-4 border-b border-[#3c4b36]">
<span class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">CONNECTION_PARAMETERS</span>
<span class="material-symbols-outlined text-[#14d1ff]" style="font-variation-settings: 'FILL' 0;">terminal</span>
</div>
<div id="connection-form-container"></div>
</div>
<div class="mt-8 text-center text-[13px] text-[#bbccb0] opacity-60">SYSTEM READY. WAITING FOR INPUT.</div>
<div class="mt-4 text-center">
<a href="https://github.com/newbietan/CloudSSH" class="text-[13px] text-[#4af626] opacity-60 hover:opacity-100 transition-colors tracking-widest uppercase">[ GitHub Open Source ]</a>
</div>
</main>
</div>
<!-- User Space Page (登录后) -->
<div id="user-space-section" class="hidden min-h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer" id="user-space-logo">CloudSSH</h1>
</div>
<div class="flex items-center gap-4">
<div id="user-info" class="flex items-center gap-3">
<!-- 动态渲染:头像 + 用户名 -->
</div>
<button id="logout-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center text-[#ffb4ab]" title="Logout">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">logout</span>
</button>
</div>
</header>
<!-- Server List Area -->
<main class="flex-1 p-6 relative overflow-auto custom-scrollbar">
<div class="scanlines pointer-events-none"></div>
<div class="max-w-5xl mx-auto">
<!-- 顶部操作栏 -->
<div class="flex justify-between items-center mb-6">
<div>
<h2 class="text-lg font-bold text-[#4af626] tracking-[0.1em]">SERVER_LIST</h2>
<p class="text-xs text-[#bbccb0] opacity-60 mt-1">Manage your SSH connections</p>
</div>
<button id="add-server-btn" class="cyber-button py-2 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_SERVER
</button>
</div>
<!-- 服务器卡片网格 -->
<div id="server-grid" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- 由 JS 动态渲染 -->
</div>
<!-- 空状态 -->
<div id="empty-state" class="hidden flex-col items-center justify-center py-20">
<span class="material-symbols-outlined text-[#3c4b36] mb-4" style="font-size: 64px; font-variation-settings: 'FILL' 0;">dns</span>
<p class="text-[#bbccb0] text-sm tracking-[0.1em] mb-2">NO_SERVERS_CONFIGURED</p>
<p class="text-[#3c4b36] text-xs mb-6">Add a server to get started</p>
<button id="empty-add-btn" class="cyber-button py-2 px-6 text-xs font-bold tracking-[0.1em] uppercase flex items-center gap-2">
<span class="material-symbols-outlined" style="font-size: 16px;">add</span>
ADD_FIRST_SERVER
</button>
</div>
</div>
</main>
<!-- Footer -->
<footer class="px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="user-copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
<!-- Server Modal (添加/编辑) -->
<div id="server-modal" class="hidden fixed inset-0 z-[100] flex items-center justify-center">
<div class="modal-overlay absolute inset-0" id="modal-backdrop"></div>
<div class="cyber-box p-6 shadow-2xl relative z-10 w-full max-w-md mx-4">
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-[#14d1ff] to-transparent opacity-50"></div>
<div class="flex items-center justify-between mb-6 pb-4 border-b border-[#3c4b36]">
<span id="modal-title" class="text-xs font-bold tracking-[0.1em] text-[#14d1ff]">ADD_SERVER</span>
<button id="modal-close-btn" class="text-[#bbccb0] hover:text-[#4af626] transition-colors cursor-pointer">
<span class="material-symbols-outlined" style="font-size: 20px;">close</span>
</button>
</div>
<form id="server-form" class="space-y-4">
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">SERVER_NAME</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">label</span>
<input id="server-name" class="terminal-input text-[13px]" placeholder="My Server" type="text" required>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<div class="col-span-3">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">HOST_ADDRESS</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">&gt;</span>
<input id="server-host" class="terminal-input text-[13px]" placeholder="192.168.1.1" type="text" required>
</div>
</div>
<div class="col-span-1">
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">PORT</label>
<div class="flex items-center">
<span class="text-[#bbccb0] mr-2">:</span>
<input id="server-port" class="terminal-input text-[13px]" placeholder="22" type="text" value="22">
</div>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_USER</label>
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">person</span>
<input id="server-username" class="terminal-input text-[13px]" placeholder="root" type="text" required>
</div>
</div>
<div>
<label class="block text-xs font-bold tracking-[0.1em] text-[#bbccb0] mb-2">AUTH_METHOD</label>
<div class="flex gap-2 mb-3">
<button type="button" id="modal-auth-tab-password" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#4af626] text-[#4af626] bg-transparent cursor-pointer transition-all" style="background:#4af626;color:#0a0a0a;">PASSWORD</button>
<button type="button" id="modal-auth-tab-key" class="auth-tab px-3 py-1 text-[11px] font-bold tracking-[0.1em] border border-[#3c4b36] text-[#bbccb0] bg-transparent cursor-pointer transition-all">PRIVATE_KEY</button>
</div>
<div id="modal-password-section">
<div class="flex items-center">
<span class="material-symbols-outlined text-[#bbccb0] mr-2" style="font-size: 16px;">key</span>
<input id="server-password" class="terminal-input text-[13px]" placeholder="••••••••" type="password">
</div>
</div>
<div id="modal-key-section" style="display:none;">
<textarea id="server-private-key" class="terminal-input text-[11px] w-full" rows="4" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...粘贴私钥内容...&#10;-----END OPENSSH PRIVATE KEY-----" style="resize:vertical;border:1px solid #3c4b36;padding:8px;"></textarea>
</div>
</div>
<div class="pt-2">
<button id="server-submit-btn" class="cyber-button w-full py-3 px-4 text-xs font-bold tracking-[0.1em] uppercase flex items-center justify-center gap-2 bg-[#4af626] text-[#022100]" type="button">
<span class="material-symbols-outlined" style="font-size: 18px;">save</span>
SAVE_SERVER
</button>
</div>
</form>
</div>
</div>
<!-- Terminal Page -->
<div id="terminal-section" class="hidden h-screen flex-col bg-background text-on-surface">
<!-- TopAppBar -->
<header class="flex justify-between items-center w-full px-6 h-16 bg-background border-b border-outline-variant z-50">
<div class="flex items-center gap-6">
<h1 class="text-2xl font-bold text-primary-container tracking-tighter cursor-pointer">CloudSSH</h1>
</div>
<div class="flex items-center gap-4 text-primary-container">
<select id="theme-selector" class="bg-surface-variant text-on-surface-variant text-xs py-1 px-2 border-none outline-none cursor-pointer rounded">
<option value="cyberpunk">Cyberpunk</option>
<option value="glacier">Glacier</option>
<option value="gruvbox">Gruvbox</option>
</select>
<button id="disconnect-btn" class="hover:opacity-80 transition-opacity cursor-pointer p-1 flex items-center justify-center" title="Disconnect">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">power_settings_new</span>
</button>
</div>
</header>
<!-- Main Terminal -->
<main class="flex-1 p-6 pb-14 relative bg-surface-container-lowest overflow-hidden">
<div class="absolute inset-0 scanline pointer-events-none z-10 opacity-30"></div>
<div id="terminal-wrapper" class="h-full w-full border border-outline-variant bg-[#0a0a0a] p-4 overflow-hidden font-code text-[13px] text-primary-container relative z-0 flex flex-col">
<!-- Status Bar -->
<div class="flex justify-between items-center border-b border-surface-variant pb-2 mb-4 text-on-surface-variant opacity-80 text-[11px]">
<div class="flex gap-6">
<span id="term-status" class="flex items-center gap-1"><div class="w-2 h-2 bg-primary-container"></div> Connected</span>
<span id="term-host">Host: -</span>
<span id="term-user">User: -</span>
<span id="term-port">Port: -</span>
</div>
<div>
<span id="term-info"></span>
</div>
</div>
<!-- Terminal Output -->
<div id="terminal-container" class="flex-1 overflow-hidden"></div>
</div>
</main>
<!-- Footer -->
<footer class="fixed bottom-0 left-0 w-full px-6 py-2 flex justify-between items-center z-50 bg-background/80 backdrop-blur-md border-t border-outline-variant text-xs h-12">
<div class="text-secondary-container opacity-60">© <span id="copyright-year"></span> CLOUD_SSH_TERMINAL - https://github.com/newbietan/CloudSSH</div>
<div class="flex gap-4">
<a class="text-on-surface-variant opacity-60 hover:opacity-100 transition-opacity" href="https://github.com/newbietan/CloudSSH">Documentation</a>
</div>
</footer>
</div>
</div>
</body>
</html>
`;