124543d658
- Agent studioE5 standalone en Go (console + systray) - VPN on-demand via tailscaled + tailscale up (authkey Headscale) - Resolver/serveur dans le tailnet studioe5 - Caddy on-demand TLS pour les instances - Nouveaux endpoints serveur /api/internal/send-to-node - Suppression des anciens binaires edubox-agent - Suivi dans SUIVI_VPN_ONDEMAND.md
243 lines
10 KiB
HTML
243 lines
10 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="fr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>studioE5 Agent</title>
|
|
<style>
|
|
* { box-sizing: border-box; }
|
|
body { font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif; background: #f1f5f9; margin: 0; padding: 2rem; color: #1e293b; }
|
|
.container { max-width: 640px; margin: 0 auto; }
|
|
.card { background: white; border-radius: 12px; padding: 1.5rem; box-shadow: 0 1px 3px rgba(0,0,0,0.1); margin-bottom: 1rem; }
|
|
h1 { font-size: 1.5rem; margin: 0 0 1rem; }
|
|
h2 { font-size: 1.125rem; margin: 0 0 1rem; }
|
|
label { display: block; font-size: 0.85rem; font-weight: 600; margin-bottom: 0.25rem; color: #475569; }
|
|
input { width: 100%; padding: 0.6rem; border: 1px solid #cbd5e1; border-radius: 8px; margin-bottom: 0.75rem; font-size: 1rem; }
|
|
input:read-only { background: #f1f5f9; }
|
|
button { width: 100%; padding: 0.7rem; background: #2563eb; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: 600; font-size: 1rem; }
|
|
button:hover { background: #1d4ed8; }
|
|
button.secondary { background: #e2e8f0; color: #1e293b; }
|
|
button.secondary:hover { background: #cbd5e1; }
|
|
.status { margin-top: 0.75rem; font-size: 0.9rem; min-height: 1.2rem; }
|
|
.success { color: #16a34a; }
|
|
.error { color: #dc2626; }
|
|
.info { color: #64748b; }
|
|
.instance-list { display: flex; flex-direction: column; gap: 0.75rem; }
|
|
.instance-item { border: 1px solid #e2e8f0; border-radius: 8px; padding: 1rem; display: flex; align-items: center; justify-content: space-between; gap: 1rem; }
|
|
.instance-meta { flex: 1; }
|
|
.instance-name { font-weight: 600; margin-bottom: 0.25rem; }
|
|
.instance-port { font-size: 0.85rem; color: #64748b; }
|
|
.badge { display: inline-block; padding: 0.25rem 0.6rem; border-radius: 999px; font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
|
|
.badge-running { background: #dcfce7; color: #166534; }
|
|
.badge-starting { background: #fef9c3; color: #854d0e; }
|
|
.badge-stopped { background: #f1f5f9; color: #475569; }
|
|
.badge-error { background: #fee2e2; color: #991b1b; }
|
|
.instance-link { font-size: 0.875rem; color: #2563eb; text-decoration: none; font-weight: 500; }
|
|
.instance-link:hover { text-decoration: underline; }
|
|
.empty { text-align: center; color: #64748b; padding: 1rem 0; }
|
|
.toolbar { display: flex; gap: 0.5rem; margin-top: 1rem; }
|
|
.toolbar button { flex: 1; }
|
|
.note { font-size: 0.8rem; color: #64748b; margin-top: 0.5rem; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<div id="home-card" class="card">
|
|
<h1>studioE5 Agent</h1>
|
|
<div id="main">
|
|
<p class="info">Connexion en cours...</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="settings-card" class="card" style="display:none;">
|
|
<h2>Paramètres</h2>
|
|
<form id="settings-form" onsubmit="saveSettings(event)">
|
|
<label for="cfg-server">Serveur WebSocket</label>
|
|
<input type="text" id="cfg-server" placeholder="ws://localhost:3001">
|
|
|
|
<label for="cfg-node">ID du nœud</label>
|
|
<input type="text" id="cfg-node" placeholder="MON-PC">
|
|
|
|
<label for="cfg-headscale-url">URL Headscale</label>
|
|
<input type="text" id="cfg-headscale-url" placeholder="https://headscale.exemple.com">
|
|
|
|
<label for="cfg-headscale-key">Clé Headscale</label>
|
|
<input type="password" id="cfg-headscale-key" placeholder="hskey-auth-...">
|
|
|
|
<label for="cfg-data-dir">Répertoire de données</label>
|
|
<input type="text" id="cfg-data-dir" readonly>
|
|
|
|
<button type="submit">Enregistrer et redémarrer</button>
|
|
</form>
|
|
<div id="settings-status" class="status"></div>
|
|
<p class="note">Le redémarrage est nécessaire pour prendre en compte les nouveaux paramètres.</p>
|
|
</div>
|
|
|
|
<div id="instances-card" class="card" style="display:none;">
|
|
<h2>Mes instances</h2>
|
|
<div id="instances" class="instance-list"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const ws = new WebSocket('ws://' + location.host + '/ws');
|
|
const main = document.getElementById('main');
|
|
const homeCard = document.getElementById('home-card');
|
|
const settingsCard = document.getElementById('settings-card');
|
|
const instancesCard = document.getElementById('instances-card');
|
|
const instancesContainer = document.getElementById('instances');
|
|
|
|
ws.onopen = () => {
|
|
ws.send(JSON.stringify({action: 'check'}));
|
|
};
|
|
|
|
ws.onmessage = (ev) => {
|
|
const msg = JSON.parse(ev.data);
|
|
|
|
if (msg.action === 'not_activated') {
|
|
showHome();
|
|
main.innerHTML = `
|
|
<p>Entre ton code d'activation (6 caractères) :</p>
|
|
<input type="text" id="code" maxlength="6" placeholder="XXXXXX" onkeydown="if(event.key==='Enter')activate()">
|
|
<button onclick="activate()">Activer</button>
|
|
<div id="status" class="status"></div>
|
|
`;
|
|
} else if (msg.action === 'activated') {
|
|
showHome();
|
|
main.innerHTML = `
|
|
<p class="success">✅ Activé : <strong>${escapeHtml(msg.studentName || '')}</strong></p>
|
|
<p class="info">Tes instances apparaissent ci-dessous.</p>
|
|
<div class="toolbar">
|
|
<button class="secondary" onclick="showSettings()">⚙️ Paramètres</button>
|
|
</div>
|
|
`;
|
|
instancesCard.style.display = 'block';
|
|
ws.send(JSON.stringify({action: 'instances'}));
|
|
} else if (msg.action === 'activation_failed') {
|
|
const status = document.getElementById('status');
|
|
if (status) status.innerHTML = `<span class="error">❌ ${escapeHtml(msg.error || 'Code invalide')}</span>`;
|
|
} else if (msg.action === 'instances_list') {
|
|
renderInstances(msg.instances);
|
|
} else if (msg.action === 'instances_updated') {
|
|
ws.send(JSON.stringify({action: 'instances'}));
|
|
}
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
main.innerHTML = `<p class="error">Déconnecté. Recharge la page.</p>`;
|
|
};
|
|
|
|
ws.onerror = (err) => {
|
|
main.innerHTML = `<p class="error">Erreur de connexion.</p>`;
|
|
};
|
|
|
|
function activate() {
|
|
const input = document.getElementById('code');
|
|
const code = input.value.trim().toUpperCase();
|
|
if (code.length !== 6) {
|
|
document.getElementById('status').innerHTML = '<span class="error">Le code doit faire 6 caractères.</span>';
|
|
return;
|
|
}
|
|
document.getElementById('status').innerHTML = 'Activation en cours...';
|
|
ws.send(JSON.stringify({action: 'activate', code}));
|
|
}
|
|
|
|
function renderInstances(instances) {
|
|
if (!instances || instances.length === 0) {
|
|
instancesContainer.innerHTML = '<p class="empty">Aucune instance assignée.</p>';
|
|
return;
|
|
}
|
|
instancesContainer.innerHTML = instances.map(i => {
|
|
const badgeClass = i.status === 'running' ? 'badge-running' :
|
|
i.status === 'starting' ? 'badge-starting' :
|
|
i.status === 'error' ? 'badge-error' : 'badge-stopped';
|
|
const link = i.status === 'running' && i.url
|
|
? `<a class="instance-link" href="${escapeHtml(i.url)}" target="_blank" rel="noopener">Ouvrir le site →</a>`
|
|
: '';
|
|
const name = escapeHtml(i.templateName || i.id);
|
|
return `
|
|
<div class="instance-item">
|
|
<div class="instance-meta">
|
|
<div class="instance-name">${name}</div>
|
|
<div class="instance-port">Port ${i.port || '-'}</div>
|
|
</div>
|
|
<div style="text-align:right;">
|
|
<span class="badge ${badgeClass}">${i.status || 'stopped'}</span>
|
|
${link ? '<div style="margin-top:0.4rem;">' + link + '</div>' : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
async function loadSettings() {
|
|
try {
|
|
const res = await fetch('/api/config');
|
|
const cfg = await res.json();
|
|
document.getElementById('cfg-server').value = cfg.server || '';
|
|
document.getElementById('cfg-node').value = cfg.node_id || '';
|
|
document.getElementById('cfg-headscale-url').value = cfg.headscale_url || '';
|
|
document.getElementById('cfg-headscale-key').value = cfg.headscale_auth_key || '';
|
|
document.getElementById('cfg-data-dir').value = cfg.data_dir || '';
|
|
} catch (err) {
|
|
document.getElementById('settings-status').innerHTML = `<span class="error">Erreur chargement config</span>`;
|
|
}
|
|
}
|
|
|
|
async function saveSettings(event) {
|
|
event.preventDefault();
|
|
const status = document.getElementById('settings-status');
|
|
status.innerHTML = 'Enregistrement...';
|
|
const cfg = {
|
|
server: document.getElementById('cfg-server').value.trim(),
|
|
node_id: document.getElementById('cfg-node').value.trim(),
|
|
headscale_url: document.getElementById('cfg-headscale-url').value.trim(),
|
|
headscale_auth_key: document.getElementById('cfg-headscale-key').value.trim(),
|
|
data_dir: document.getElementById('cfg-data-dir').value.trim()
|
|
};
|
|
try {
|
|
const res = await fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(cfg)
|
|
});
|
|
if (res.ok) {
|
|
status.innerHTML = '<span class="success">✅ Enregistré. Redémarrage en cours...</span>';
|
|
await fetch('/api/restart', {method: 'POST'});
|
|
setTimeout(() => location.reload(), 3000);
|
|
} else {
|
|
status.innerHTML = `<span class="error">❌ Erreur ${res.status}</span>`;
|
|
}
|
|
} catch (err) {
|
|
status.innerHTML = `<span class="error">❌ ${escapeHtml(err.message)}</span>`;
|
|
}
|
|
}
|
|
|
|
function showSettings() {
|
|
homeCard.style.display = 'none';
|
|
instancesCard.style.display = 'none';
|
|
settingsCard.style.display = 'block';
|
|
loadSettings();
|
|
}
|
|
|
|
function showHome() {
|
|
homeCard.style.display = 'block';
|
|
settingsCard.style.display = 'none';
|
|
}
|
|
|
|
if (location.hash === '#settings') {
|
|
showSettings();
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
if (text == null) return '';
|
|
return String(text)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|