feat: git token management in app detail UI

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
This commit is contained in:
Claude 2026-03-26 08:32:58 +00:00
parent 0b3cbf8734
commit 4fb8c6b2c7
No known key found for this signature in database
2 changed files with 66 additions and 11 deletions

View file

@ -297,6 +297,18 @@ pub async fn app_detail(
}
};
let (git_token_status, git_token_clear_btn) = if app.git_token.is_some() {
(
r#"<span class="badge badge-success">Token configured</span>"#.to_string(),
r#"<button class="danger" onclick="clearGitToken()">Clear</button>"#.to_string(),
)
} else {
(
r#"<span class="badge badge-unknown">No token — public repos only</span>"#.to_string(),
String::new(),
)
};
let body = APP_DETAIL_TMPL
.replace("{{name}}", &app.name)
.replace("{{repo}}", &app.repo_url)
@ -308,7 +320,9 @@ pub async fn app_detail(
.replace("{{deploy_rows}}", &deploy_rows)
.replace("{{env_rows}}", &env_rows)
.replace("{{c_badge}}", &container_badge(&container_state))
.replace("{{db_card}}", &db_card);
.replace("{{db_card}}", &db_card)
.replace("{{git_token_status}}", &git_token_status)
.replace("{{git_token_clear_btn}}", &git_token_clear_btn);
Html(page(&app.name, &body)).into_response()
}

View file

@ -35,6 +35,26 @@
{{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">
@ -145,6 +165,27 @@ async function deprovisionDb() {
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'});