Hostityourself/server/templates/index.html
Claude c113b098e1
refactor: extract HTML/CSS/JS from ui.rs into template files
Move all inline markup out of ui.rs into server/templates/:
  styles.css       — shared stylesheet
  index.html       — dashboard page
  app_detail.html  — app detail page
  users.html       — users admin page

Templates are embedded at compile time via include_str! and rendered
with simple str::replace("{{placeholder}}", value) calls. JS/CSS
braces no longer need escaping, making the templates editable with
normal syntax highlighting.

https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
2026-03-24 13:03:10 +00:00

114 lines
4.6 KiB
HTML

<nav><h1>&#9749; HostItYourself</h1><span style="display:flex;gap:16px;align-items:center"><span class="muted">{{n}} app(s)</span><a href="/admin/users" class="muted" style="font-size:0.82rem">users</a><a href="/logout" class="muted" style="font-size:0.82rem">logout</a></span></nav>
<div class="card" style="padding:16px 24px">
<h2 style="margin-bottom:14px">System</h2>
<div class="grid3">
<div>
<label>CPU Load (1 min)</label>
<div class="stat-big">{{load}}</div>
</div>
<div>
<label>Memory</label>
<div class="stat-big">{{ram_used}} / {{ram_total}} MB</div>
<div class="stat-sub">{{ram_pct}}% used</div>
</div>
<div>
<label>Disk (/)</label>
<div class="stat-big">{{disk_used}} / {{disk_total}} GB</div>
<div class="stat-sub">{{disk_pct}}% used</div>
</div>
</div>
</div>
<div class="card">
<h2>Add App</h2>
<div class="grid2" style="margin-bottom:12px">
<div><label>Name (slug)</label><input id="f-name" type="text" placeholder="my-api"></div>
<div><label>GitHub Repo URL <span style="font-weight:normal;opacity:.6">(optional)</span></label><input id="f-repo" type="text" placeholder="https://github.com/you/repo.git"></div>
</div>
<div class="grid2" style="margin-bottom:16px">
<div><label>Branch</label><input id="f-branch" type="text" value="main"></div>
<div><label>Container Port</label><input id="f-port" type="number" placeholder="3000"></div>
</div>
<button class="primary" onclick="createApp()">Create App</button>
</div>
<div class="card">
<h2>Apps</h2>
<table>
<thead><tr><th>Name</th><th>Repo</th><th>Branch</th><th>Container</th><th>Last Deploy</th><th>Actions</th></tr></thead>
<tbody id="apps-body">{{rows}}</tbody>
</table>
</div>
<script>
async function createApp() {
const data = {
name: document.getElementById('f-name').value.trim(),
repo_url: document.getElementById('f-repo').value.trim(),
branch: document.getElementById('f-branch').value.trim() || 'main',
port: parseInt(document.getElementById('f-port').value),
};
if (!data.name || !data.port) { alert('Fill in all fields'); return; }
const r = await fetch('/api/apps', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(data),
});
if (r.ok) window.location.reload();
else alert('Error: ' + await r.text());
}
async function deploy(id) {
if (!confirm('Deploy ' + id + ' now?')) return;
const r = await fetch('/api/apps/' + id + '/deploy', {method: 'POST'});
if (r.ok) {
// Immediately show building; the 5 s poller will advance to the final status.
const row = document.querySelector(`tr[data-id="${id}"]`);
if (row) {
const db = row.querySelector('[data-deploy-badge]');
if (db) db.innerHTML = deployBadgeHtml('building');
}
} else {
alert('Error: ' + await r.text());
}
}
async function del(id) {
if (!confirm('Delete app "' + id + '"? This cannot be undone.')) return;
await fetch('/api/apps/' + id, {method: 'DELETE'});
window.location.reload();
}
async function stopApp(id) {
if (!confirm('Stop ' + id + '?')) return;
const r = await fetch('/api/apps/' + id + '/stop', {method: 'POST'});
if (!r.ok) alert('Error stopping app');
}
async function restartApp(id) {
if (!confirm('Restart ' + id + '?')) return;
const r = await fetch('/api/apps/' + id + '/restart', {method: 'POST'});
if (!r.ok) alert('Error restarting app');
}
function deployBadgeHtml(s) {
const cls = {success:'badge-success',failed:'badge-failed',building:'badge-building',queued:'badge-building'}[s] || 'badge-unknown';
return `<span class="badge ${cls}">${s}</span>`;
}
function containerBadgeHtml(s) {
const cls = {running:'badge-success',exited:'badge-failed',restarting:'badge-building'}[s] || 'badge-unknown';
return `<span class="badge ${cls}">${s}</span>`;
}
// Auto-refresh container + deploy statuses every 5 s.
setInterval(async () => {
const r = await fetch('/api/status');
if (!r.ok) return;
const statuses = await r.json();
Object.entries(statuses).forEach(([id, s]) => {
const row = document.querySelector(`tr[data-id="${id}"]`);
if (!row) return;
const cb = row.querySelector('[data-container-badge]');
const db = row.querySelector('[data-deploy-badge]');
if (cb) cb.innerHTML = containerBadgeHtml(s.container);
if (db) db.innerHTML = deployBadgeHtml(s.deploy);
});
}, 5000);
</script>