Improve security, accessibility, and performance
- Add Content Security Policy meta tag for XSS protection - Add width/height to image for CLS prevention - Add aria-hidden to decorative SVG icons (7 locations) - Refactor inline onclick to addEventListener for CSP compliance - Add visibility-aware setInterval for ping updates - Add keyboard navigation (Arrow keys) for tab panels Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
62
app.js
62
app.js
@@ -323,5 +323,63 @@ function updatePing() {
|
||||
});
|
||||
}
|
||||
|
||||
// Ping 업데이트 시작
|
||||
setInterval(updatePing, 2000);
|
||||
// Ping 업데이트 시작 (visibility-aware)
|
||||
let pingInterval;
|
||||
|
||||
function startPingUpdates() {
|
||||
if (!pingInterval) {
|
||||
pingInterval = setInterval(updatePing, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
function stopPingUpdates() {
|
||||
if (pingInterval) {
|
||||
clearInterval(pingInterval);
|
||||
pingInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility change handler
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.hidden) {
|
||||
stopPingUpdates();
|
||||
} else {
|
||||
startPingUpdates();
|
||||
}
|
||||
});
|
||||
|
||||
// Initial start
|
||||
startPingUpdates();
|
||||
|
||||
// Tab switching with event listeners and keyboard navigation
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const btnN8n = document.getElementById('btn-n8n');
|
||||
const btnTf = document.getElementById('btn-tf');
|
||||
|
||||
if (btnN8n && btnTf) {
|
||||
// Click event listeners
|
||||
btnN8n.addEventListener('click', () => switchTab('n8n'));
|
||||
btnTf.addEventListener('click', () => switchTab('tf'));
|
||||
|
||||
// Keyboard navigation (Arrow keys)
|
||||
[btnN8n, btnTf].forEach(btn => {
|
||||
btn.addEventListener('keydown', (e) => {
|
||||
const currentTab = btn.getAttribute('data-tab');
|
||||
|
||||
if (e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
if (currentTab === 'n8n') {
|
||||
btnTf.focus();
|
||||
switchTab('tf');
|
||||
}
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
if (currentTab === 'tf') {
|
||||
btnN8n.focus();
|
||||
switchTab('n8n');
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
23
index.html
23
index.html
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://cdn.jsdelivr.net 'unsafe-inline'; style-src 'self' https://fonts.googleapis.com 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; img-src 'self' data:; connect-src 'self';">
|
||||
<title>Anvil Hosting - 개발자를 위한 컨테이너 클라우드</title>
|
||||
|
||||
<!-- SEO Meta Tags -->
|
||||
@@ -66,7 +67,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<a href="https://t.me/AnvilForgeBot" target="_blank" rel="noopener noreferrer" aria-label="텔레그램 봇으로 콘솔 시작" class="px-4 py-2 bg-brand-600 hover:bg-brand-500 text-white text-sm font-bold rounded-lg transition shadow-lg shadow-brand-500/20 flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.62-.2-1.12-.31-1.15-.63.03-.37.59-.75 1.5-.95 6.07-2.64 10.12-4.38 12.15-5.21 2.91-1.2 3.51-1.4 3.91-1.41.09 0 .28.02.41.09.11.06.23.14.3.24.08.12.12.33.09.57z"/></svg>
|
||||
<svg class="w-4 h-4" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.62-.2-1.12-.31-1.15-.63.03-.37.59-.75 1.5-.95 6.07-2.64 10.12-4.38 12.15-5.21 2.91-1.2 3.51-1.4 3.91-1.41.09 0 .28.02.41.09.11.06.23.14.3.24.08.12.12.33.09.57z"/></svg>
|
||||
Console 시작
|
||||
</a>
|
||||
</div>
|
||||
@@ -103,7 +104,7 @@
|
||||
<button @click="openLauncher()" :aria-expanded="launcherOpen" aria-haspopup="dialog" aria-label="서버 런처 열기" class="btn-glow relative w-full sm:w-auto px-8 py-4 bg-gradient-to-r from-white to-slate-100 text-dark-900 font-bold rounded-xl flex items-center justify-center gap-3 transition-all active:scale-95 group">
|
||||
<span class="text-xl">🚀</span>
|
||||
<span>인스턴스 즉시 배포</span>
|
||||
<svg class="w-4 h-4 text-brand-600 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
|
||||
<svg class="w-4 h-4 text-brand-600 transition-transform group-hover:translate-x-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="ping-widget" class="flex flex-col gap-1.5 text-xs font-mono text-slate-400 bg-slate-900/50 p-3 rounded-lg border border-slate-700/50 backdrop-blur-sm mt-8 sm:mt-0 sm:ml-4 max-w-sm">
|
||||
@@ -143,8 +144,8 @@
|
||||
<div class="text-xs text-[#6c7883]">bot</div>
|
||||
</div>
|
||||
<div class="flex gap-4 text-[#6c7883]">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M15.5 14h-.79l-.28-.27a6.5 6.5 0 0 0 1.48-5.34c-.47-2.78-2.79-5-5.59-5.34a6.505 6.505 0 0 0-7.27 7.27c.34 2.8 2.56 5.12 5.34 5.59a6.5 6.5 0 0 0 5.34-1.48l.27.28v.79l4.25 4.25c.41.41 1.08.41 1.49 0 .41-.41.41-1.08 0-1.49L15.5 14zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -285,7 +286,7 @@
|
||||
<ul class="space-y-4">
|
||||
<li class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded bg-slate-800 flex items-center justify-center text-purple-400 border border-slate-700">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-white">Webhook Integration</div>
|
||||
@@ -294,7 +295,7 @@
|
||||
</li>
|
||||
<li class="flex items-center gap-4">
|
||||
<div class="w-10 h-10 rounded bg-slate-800 flex items-center justify-center text-purple-400 border border-slate-700">
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/></svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-bold text-white">Full API Access</div>
|
||||
@@ -310,7 +311,7 @@
|
||||
<button role="tab"
|
||||
aria-selected="true"
|
||||
aria-controls="panel-n8n"
|
||||
onclick="switchTab('n8n')"
|
||||
data-tab="n8n"
|
||||
id="btn-n8n"
|
||||
class="px-4 py-2 rounded-lg bg-purple-600 text-white text-sm font-bold transition">
|
||||
n8n Workflow
|
||||
@@ -318,7 +319,7 @@
|
||||
<button role="tab"
|
||||
aria-selected="false"
|
||||
aria-controls="panel-tf"
|
||||
onclick="switchTab('tf')"
|
||||
data-tab="tf"
|
||||
id="btn-tf"
|
||||
class="px-4 py-2 rounded-lg bg-slate-800 text-slate-400 text-sm font-bold border border-slate-700 hover:text-white transition">
|
||||
Terraform
|
||||
@@ -339,6 +340,8 @@
|
||||
<img src="images/n8n-workflow.png"
|
||||
alt="n8n 워크플로우 자동화 예시: GitHub Actions 트리거 → Anvil API 호출 → 서버 자동 생성 프로세스"
|
||||
class="w-full h-auto block transition-transform duration-500 group-hover:scale-[1.02]"
|
||||
width="800"
|
||||
height="450"
|
||||
loading="lazy"
|
||||
crossorigin="anonymous">
|
||||
</div>
|
||||
@@ -668,7 +671,7 @@
|
||||
<p class="text-[10px] text-slate-400 mt-1">※ 서울/도쿄 리전 재고 및 가격은 봇으로 확인하세요.</p>
|
||||
</div>
|
||||
<a href="https://t.me/AnvilForgeBot" target="_blank" rel="noopener noreferrer" class="btn-glow px-8 py-4 bg-gradient-to-r from-brand-600 to-brand-500 text-white font-bold rounded-xl hover:shadow-lg hover:shadow-brand-500/30 transition-all flex items-center gap-3">
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.62-.2-1.12-.31-1.15-.63.03-.37.59-.75 1.5-.95 6.07-2.64 10.12-4.38 12.15-5.21 2.91-1.2 3.51-1.4 3.91-1.41.09 0 .28.02.41.09.11.06.23.14.3.24.08.12.12.33.09.57z"/></svg>
|
||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm4.64 6.8c-.15 1.58-.8 5.42-1.13 7.19-.14.75-.42 1-.68 1.03-.58.05-1.02-.38-1.58-.75-.88-.58-1.38-.94-2.23-1.5-.99-.65-.35-1.01.22-1.59.15-.15 2.71-2.48 2.76-2.69a.2.2 0 00-.05-.18c-.06-.05-.14-.03-.21-.02-.09.02-1.49.95-4.22 2.79-.4.27-.76.41-1.08.4-.36-.01-1.04-.2-1.55-.37-.62-.2-1.12-.31-1.15-.63.03-.37.59-.75 1.5-.95 6.07-2.64 10.12-4.38 12.15-5.21 2.91-1.2 3.51-1.4 3.91-1.41.09 0 .28.02.41.09.11.06.23.14.3.24.08.12.12.33.09.57z"/></svg>
|
||||
실시간 재고/가격 조회 (Bot)
|
||||
</a>
|
||||
</div>
|
||||
@@ -884,7 +887,7 @@
|
||||
<!-- Back Button -->
|
||||
<div x-show="wizardStep > 0 && wizardStep < 5" class="px-4 pb-3 flex-shrink-0">
|
||||
<button @click="goBack" class="text-xs text-slate-500 hover:text-slate-300 transition flex items-center gap-1">
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
<svg class="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
|
||||
이전 단계
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user