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
114 lines
4.6 KiB
HTML
114 lines
4.6 KiB
HTML
<nav><h1>☕ 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>
|