initialize XIVLauncher Remote OTP project with web interface and Docker support

This commit is contained in:
CPTN Cosmo 2026-04-18 16:21:51 +02:00
commit 6ce356ffed
8 changed files with 717 additions and 0 deletions

117
static/app.js Normal file
View file

@ -0,0 +1,117 @@
document.addEventListener('DOMContentLoaded', () => {
const addForm = document.getElementById('add-form');
const instancesList = document.getElementById('instances-list');
const refreshBtn = document.getElementById('refresh-btn');
const portInput = document.getElementById('port');
const ufwCmd = document.getElementById('ufw-cmd');
const iptablesCmd = document.getElementById('iptables-cmd');
portInput.addEventListener('input', (e) => {
const port = e.target.value || '4646';
ufwCmd.textContent = `sudo ufw allow ${port}/tcp`;
iptablesCmd.textContent = `sudo iptables -I INPUT -p tcp --dport ${port} -j ACCEPT`;
});
const fetchInstances = async () => {
try {
const res = await fetch('/api/instances');
if (res.status === 401) {
return; // Let browser handle auth
}
const data = await res.json();
renderInstances(data);
} catch (e) {
console.error('Failed to fetch instances:', e);
}
};
const renderInstances = (instances) => {
if (instances.length === 0) {
instancesList.innerHTML = '<div class="empty-state">No instances configured yet.</div>';
return;
}
instancesList.innerHTML = instances.map(inst => {
let statusClass = 'status-unknown';
if (inst.last_status === 'Sent') statusClass = 'status-sent';
else if (inst.last_status === 'Offline') statusClass = 'status-offline';
else if (inst.last_status.startsWith('Failed')) statusClass = 'status-offline';
const timeAgo = inst.last_checked ? Math.round((Date.now()/1000 - inst.last_checked)) + 's ago' : 'Never';
return `
<div class="instance-item">
<div class="instance-info">
<h3>${escapeHtml(inst.name)}</h3>
<div class="instance-meta">
<span>${escapeHtml(inst.ip)}:${inst.port}</span>
<span class="status-badge ${statusClass}">${escapeHtml(inst.last_status)}</span>
<span>Last checked: ${timeAgo}</span>
</div>
</div>
<button class="delete-btn" onclick="deleteInstance(${inst.id})" title="Delete">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
</div>
`;
}).join('');
};
addForm.addEventListener('submit', async (e) => {
e.preventDefault();
const name = document.getElementById('name').value;
const ip = document.getElementById('ip').value;
const port = parseInt(document.getElementById('port').value, 10);
const secret = document.getElementById('secret').value;
try {
const res = await fetch('/api/instances', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name, ip, port, secret })
});
if (res.ok) {
addForm.reset();
document.getElementById('port').value = "4646";
fetchInstances();
} else {
alert('Failed to add instance');
}
} catch (e) {
console.error('Error adding instance', e);
}
});
refreshBtn.addEventListener('click', fetchInstances);
window.deleteInstance = async (id) => {
if (!confirm('Are you sure you want to delete this instance?')) return;
try {
const res = await fetch(`/api/instances/${id}`, { method: 'DELETE' });
if (res.ok) {
fetchInstances();
}
} catch (e) {
console.error('Error deleting instance', e);
}
};
function escapeHtml(unsafe) {
if (!unsafe) return '';
return unsafe.toString()
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
fetchInstances();
setInterval(fetchInstances, 5000);
});