Adds a 'Git Authentication' card to the app detail page with: - Status badge (Token configured / No token) - Password input to set/update the token - Clear button (only shown when a token is stored) Token is saved/cleared via PATCH /api/apps/:id — no new endpoints needed. https://claude.ai/code/session_01FKCW3FDjNFj6jve4niMFXH
199 lines
7.8 KiB
HTML
199 lines
7.8 KiB
HTML
<nav>
|
|
<h1><a href="/" style="color:inherit">HIY</a> / {{name}}</h1>
|
|
<div style="display:flex;gap:8px;align-items:center">
|
|
{{c_badge}}
|
|
<button class="primary" onclick="deploy()">Deploy Now</button>
|
|
<button onclick="stopApp()">Stop</button>
|
|
<button onclick="restartApp()">Restart</button>
|
|
<a href="/admin/users" class="muted" style="font-size:0.82rem;padding:0 4px">users</a>
|
|
<a href="/logout" class="muted" style="font-size:0.82rem;padding:0 4px">logout</a>
|
|
</div>
|
|
</nav>
|
|
<p class="subtitle">
|
|
<a href="{{repo}}" target="_blank">{{repo}}</a>
|
|
· branch <code>{{branch}}</code>
|
|
· port <code>{{port}}</code>
|
|
· <a href="http://{{name}}.{{host}}" target="_blank">{{name}}.{{host}}</a>
|
|
</p>
|
|
|
|
<div class="card">
|
|
<h2>Deploy History</h2>
|
|
<table>
|
|
<thead><tr><th>SHA</th><th>Status</th><th>Triggered By</th><th>Time</th><th></th></tr></thead>
|
|
<tbody>{{deploy_rows}}</tbody>
|
|
</table>
|
|
<div id="log-panel" style="display:none;margin-top:16px">
|
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px">
|
|
<h2 id="log-title">Build Log</h2>
|
|
<a id="back-btn" href="/" style="display:none">
|
|
<button>← Dashboard</button>
|
|
</a>
|
|
</div>
|
|
<pre id="log-out"></pre>
|
|
</div>
|
|
</div>
|
|
|
|
{{db_card}}
|
|
|
|
<div class="card">
|
|
<h2>Git Authentication</h2>
|
|
<p class="muted" style="margin-bottom:12px;font-size:0.9rem">
|
|
Required for private repos. Store a Personal Access Token (GitHub: <em>repo</em> scope,
|
|
GitLab: <em>read_repository</em>) so deploys can clone without interactive prompts.
|
|
Only HTTPS repo URLs are supported; SSH URLs use the server's own key pair.
|
|
</p>
|
|
<p style="margin-bottom:12px">{{git_token_status}}</p>
|
|
<div class="row">
|
|
<div style="flex:1">
|
|
<label>Personal Access Token</label>
|
|
<input id="git-token-input" type="password" placeholder="ghp_…">
|
|
</div>
|
|
<div style="align-self:flex-end;display:flex;gap:8px">
|
|
<button class="primary" onclick="saveGitToken()">Save</button>
|
|
{{git_token_clear_btn}}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Environment Variables</h2>
|
|
<div class="row" style="margin-bottom:16px">
|
|
<div style="flex:1"><label>Key</label><input id="ev-key" type="text" placeholder="DATABASE_URL"></div>
|
|
<div style="flex:2"><label>Value</label><input id="ev-val" type="password" placeholder="secret"></div>
|
|
<div style="align-self:flex-end"><button class="primary" onclick="setEnv()">Set</button></div>
|
|
</div>
|
|
<table>
|
|
<thead><tr><th>Key</th><th>Value</th><th></th></tr></thead>
|
|
<tbody id="env-body">{{env_rows}}</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>GitHub Webhook</h2>
|
|
<p style="margin-bottom:8px">Configure GitHub → Settings → Webhooks → Add webhook:</p>
|
|
<table>
|
|
<tr><td style="width:140px">Payload URL</td><td><code>http(s)://YOUR_DOMAIN/webhook/{{app_id}}</code></td></tr>
|
|
<tr><td>Content type</td><td><code>application/json</code></td></tr>
|
|
<tr><td>Secret</td><td><code>{{secret}}</code></td></tr>
|
|
<tr><td>Events</td><td>Just the <em>push</em> event</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
<script>
|
|
const APP_ID = '{{app_id}}';
|
|
|
|
// Deploy logs are collapsed by default; only open when triggered.
|
|
|
|
async function deploy() {
|
|
const r = await fetch('/api/apps/' + APP_ID + '/deploy', {method:'POST'});
|
|
if (r.ok) { const d = await r.json(); showLog(d.id); }
|
|
else alert('Deploy failed: ' + await r.text());
|
|
}
|
|
async function showLog(deployId) {
|
|
const panel = document.getElementById('log-panel');
|
|
const out = document.getElementById('log-out');
|
|
const title = document.getElementById('log-title');
|
|
panel.style.display = 'block';
|
|
out.textContent = 'Loading…';
|
|
panel.scrollIntoView({behavior:'smooth'});
|
|
|
|
// Fetch current deploy state first.
|
|
const r = await fetch('/api/deploys/' + deployId);
|
|
if (!r.ok) { out.textContent = 'Could not load deploy ' + deployId; return; }
|
|
const deploy = await r.json();
|
|
|
|
title.textContent = 'Build Log — ' + deploy.status;
|
|
title.style.color = deploy.status === 'success' ? '#4ade80'
|
|
: deploy.status === 'failed' ? '#f87171' : '#fb923c';
|
|
|
|
// Already finished — just render the stored log, no SSE needed.
|
|
if (deploy.status === 'success' || deploy.status === 'failed') {
|
|
out.textContent = deploy.log || '(no output captured)';
|
|
out.scrollTop = out.scrollHeight;
|
|
document.getElementById('back-btn').style.display = 'inline-block';
|
|
return;
|
|
}
|
|
|
|
// Still running — stream updates via SSE.
|
|
out.textContent = '';
|
|
const es = new EventSource('/api/deploys/' + deployId + '/logs');
|
|
es.onmessage = e => {
|
|
out.textContent += e.data;
|
|
out.scrollTop = out.scrollHeight;
|
|
};
|
|
es.addEventListener('done', e => {
|
|
es.close();
|
|
title.textContent = 'Build Log — ' + e.data;
|
|
title.style.color = e.data === 'success' ? '#4ade80' : '#f87171';
|
|
document.getElementById('back-btn').style.display = 'inline-block';
|
|
});
|
|
es.onerror = () => {
|
|
es.close();
|
|
// Fallback: re-fetch the finished log.
|
|
fetch('/api/deploys/' + deployId)
|
|
.then(r => r.json())
|
|
.then(d => {
|
|
out.textContent = d.log || out.textContent;
|
|
title.textContent = 'Build Log — ' + d.status;
|
|
});
|
|
};
|
|
}
|
|
async function setEnv() {
|
|
const key = document.getElementById('ev-key').value.trim();
|
|
const val = document.getElementById('ev-val').value;
|
|
if (!key) { alert('Key required'); return; }
|
|
await fetch('/api/apps/' + APP_ID + '/env', {
|
|
method:'POST',
|
|
headers:{'Content-Type':'application/json'},
|
|
body: JSON.stringify({key, value: val}),
|
|
});
|
|
window.location.reload();
|
|
}
|
|
async function removeEnv(key) {
|
|
if (!confirm('Remove ' + key + '?')) return;
|
|
await fetch('/api/apps/' + APP_ID + '/env/' + key, {method:'DELETE'});
|
|
window.location.reload();
|
|
}
|
|
async function provisionDb() {
|
|
const r = await fetch('/api/apps/' + APP_ID + '/database', {method: 'POST'});
|
|
if (r.ok) window.location.reload();
|
|
else alert('Error: ' + await r.text());
|
|
}
|
|
async function deprovisionDb() {
|
|
if (!confirm('Drop the database and all its data for ' + APP_ID + '?\nThis cannot be undone.')) return;
|
|
const r = await fetch('/api/apps/' + APP_ID + '/database', {method: 'DELETE'});
|
|
if (r.ok) window.location.reload();
|
|
else alert('Error: ' + await r.text());
|
|
}
|
|
async function saveGitToken() {
|
|
const tok = document.getElementById('git-token-input').value;
|
|
if (!tok) { alert('Enter a token first'); return; }
|
|
const r = await fetch('/api/apps/' + APP_ID, {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({git_token: tok}),
|
|
});
|
|
if (r.ok) window.location.reload();
|
|
else alert('Error saving token: ' + await r.text());
|
|
}
|
|
async function clearGitToken() {
|
|
if (!confirm('Remove the stored git token for ' + APP_ID + '?')) return;
|
|
const r = await fetch('/api/apps/' + APP_ID, {
|
|
method: 'PATCH',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({git_token: ''}),
|
|
});
|
|
if (r.ok) window.location.reload();
|
|
else alert('Error clearing token: ' + await r.text());
|
|
}
|
|
async function stopApp() {
|
|
if (!confirm('Stop ' + APP_ID + '?')) return;
|
|
const r = await fetch('/api/apps/' + APP_ID + '/stop', {method:'POST'});
|
|
if (!r.ok) alert('Error stopping app');
|
|
}
|
|
async function restartApp() {
|
|
if (!confirm('Restart ' + APP_ID + '?')) return;
|
|
const r = await fetch('/api/apps/' + APP_ID + '/restart', {method:'POST'});
|
|
if (!r.ok) alert('Error restarting app');
|
|
}
|
|
</script>
|