extra-forges: fully dashboard-provisioned, no host config
Per mara's feedback on PR #2407 ("better: you can also provide url in dashboard, same as with matrix, no host config"), drops services.hyperhive.extraForges and the admin-API mint/revoke flow entirely. The operator now creates a token on the external forge themselves and pastes a label + base URL + access token into the dashboard's FORGES tab, the same shape as the GitHub PAT flow plus the base-URL field from the matrix extra-account flow. hive-c0re only ever writes/deletes two local files per account (forge-<label>-token, forge-<label>.json sidecar for the URL) via hive-priv — no remote account creation, no admin token, no revoke-on-the-remote-side, no nix config to enumerate. - nix/host-modules/hive-forge/default.nix: removed the extraForges option, its label-format assertion, and the HYPERHIVE_EXTRA_FORGES env forwarding. - hive-c0re/src/forge/extra.rs: deleted (REST admin-API provisioning, no longer needed). - hive-c0re/src/dashboard/extra_forges.rs: GET /api/extra-forges? agent= lists an agent's stored forges by scanning its state dir (mirrors matrix_accounts.rs's filename-scan listing), POST /api/extra-forge-account (agent/label/base_url/token/ action=add|remove) stores or removes an account. - hive-sh4re/priv_proto.rs + hive-priv/main.rs: new WriteAgentExtraForgeAccount/DeleteAgentExtraForgeAccount priv requests (adds base_url, writes/deletes a JSON sidecar alongside the token). - hive-c0re/src/priv_client.rs: matching wrapper functions. - frontend/packages/dashboard/src/credentials.{html,js}: FORGES tab is a per-agent list + add-account paste form (label/base_url/token), no grant/revoke-from-catalog UI. - docs/web-ui/dashboard.md: FORGES tab section rewritten. Supersedes the design in PR #2407 (already approved+green on the old admin-API model) — opening as a fresh PR against the same issues rather than force-pushing over the approved one.
This commit is contained in:
parent
f025f32ccb
commit
dbf880ac66
10 changed files with 558 additions and 2 deletions
|
|
@ -26,6 +26,10 @@
|
|||
aria-controls="cred-pane-github" data-tab="github">
|
||||
<span class="cred-tab-label">GITHUB</span>
|
||||
</a>
|
||||
<a class="hive-tab" id="cred-tab-forges" href="#forges" role="tab"
|
||||
aria-controls="cred-pane-forges" data-tab="forges">
|
||||
<span class="cred-tab-label">FORGES</span>
|
||||
</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
|
|
@ -125,6 +129,38 @@
|
|||
</form>
|
||||
</section>
|
||||
|
||||
<!-- FORGES tab: external Forgejo/Gitea/Codeberg-compatible forges.
|
||||
Entirely dashboard-provisioned, no host-side nix config — same
|
||||
shape as GITHUB plus a base-URL field (like MATRIX's homeserver).
|
||||
The operator creates a token on the external forge themselves
|
||||
(however that forge lets them) and pastes label + URL + token
|
||||
below. No remote account minting/revoking — purely local. -->
|
||||
<section class="cred-pane" id="cred-pane-forges" data-tab-pane="forges"
|
||||
role="tabpanel" aria-labelledby="cred-tab-forges" hidden>
|
||||
<p class="meta">store a <strong>label + base URL + access token</strong> for an external Forgejo/Gitea/Codeberg-compatible forge, per agent. no account is created on the remote forge — create the token there yourself first. the token is never displayed back on this page.</p>
|
||||
|
||||
<h3>◇ provisioned forges</h3>
|
||||
<div id="ef-list" class="ef-list"><p class="meta">select an agent to see its forge accounts.</p></div>
|
||||
|
||||
<h3>◇ add forge account</h3>
|
||||
<form id="ef-form" class="ma-form" autocomplete="off">
|
||||
<label class="ma-field">
|
||||
<span>label</span>
|
||||
<input type="text" name="label" placeholder="e.g. codeberg" required>
|
||||
</label>
|
||||
<label class="ma-field">
|
||||
<span>base url</span>
|
||||
<input type="text" name="base_url" placeholder="https://codeberg.org" required>
|
||||
</label>
|
||||
<label class="ma-field">
|
||||
<span>access token</span>
|
||||
<input type="password" name="token" autocomplete="off" required>
|
||||
</label>
|
||||
<button type="submit" class="btn btn-spawn">store account</button>
|
||||
<p id="ef-result" class="ma-result" aria-live="polite"></p>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script type="module" src="/static/credentials.js" defer></script>
|
||||
|
|
|
|||
|
|
@ -10,10 +10,18 @@
|
|||
// {ok:true}; same error_response shape as matrix-account-login).
|
||||
// No account name / homeserver / login mode, and no
|
||||
// live/heartbeat concept for a static PAT — just present/absent.
|
||||
// FORGES — external forge accounts, entirely dashboard-provisioned (no
|
||||
// host-side config): GET /api/extra-forges?agent= lists the
|
||||
// agent's stored {label, base_url} pairs, POST
|
||||
// /api/extra-forge-account (form agent/label/base_url/token/
|
||||
// action=add|remove) stores or removes one. No remote account
|
||||
// creation — the operator makes the token on the external forge
|
||||
// themselves and pastes it in, same trust model as GITHUB.
|
||||
// Per-tab detail comments live next to their section below.
|
||||
|
||||
import { $, el, esc, fmtAgeSecs, renderServerWarnings } from './common.js';
|
||||
import { createTabStrip } from '@hive/shared/tabs.js';
|
||||
import { themedConfirm, themedToast } from './modal.js';
|
||||
|
||||
let agents = [];
|
||||
// agent name → container running (bool), from /api/state. Cross-referenced by
|
||||
|
|
@ -342,11 +350,149 @@ async function submitGithub(e) {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── FORGES tab ─────────────────────────────────────────────────────────
|
||||
// Entirely dashboard-provisioned, no host-side nix config: per-agent list
|
||||
// (GET /api/extra-forges?agent=, derived from the agent's own
|
||||
// forge-<label>-token files) + an add form (POST /api/extra-forge-account,
|
||||
// form label/base_url/token, action=add) and a remove button per row
|
||||
// (same POST, action=remove). No remote account creation — purely local
|
||||
// bookkeeping for a token the operator already created on the external
|
||||
// forge themselves.
|
||||
|
||||
async function loadForgeAccounts(agent) {
|
||||
const list = $('ef-list');
|
||||
if (!agent) {
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'select an agent to see its forge accounts.'));
|
||||
return;
|
||||
}
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'loading…'));
|
||||
let forges;
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forges?agent=' + encodeURIComponent(agent));
|
||||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||||
forges = (await resp.json()).forges || [];
|
||||
} catch (err) {
|
||||
list.replaceChildren(el('p', { class: 'err' },
|
||||
'could not load forge accounts: ' + esc(String(err)) + ' (the backend endpoint may not be deployed yet).'));
|
||||
return;
|
||||
}
|
||||
|
||||
list.replaceChildren();
|
||||
if (!forges.length) {
|
||||
list.replaceChildren(el('p', { class: 'meta' }, 'no forge accounts stored for this agent.'));
|
||||
return;
|
||||
}
|
||||
const ul = el('ul', { class: 'ma-accounts' });
|
||||
for (const forge of forges) {
|
||||
const btn = el('button', { class: 'btn', type: 'button' }, 'remove');
|
||||
btn.addEventListener('click', () => onForgeRemoveClick(agent, forge, btn));
|
||||
ul.append(el('li', { class: 'ma-account' },
|
||||
el('span', { class: 'ma-dot ok' }),
|
||||
el('span', { class: 'ma-name' }, forge.label),
|
||||
el('span', { class: 'ma-hs' }, forge.base_url || '—'),
|
||||
el('span', { class: 'ma-status ok' }, 'token stored ✓'),
|
||||
btn,
|
||||
));
|
||||
}
|
||||
list.append(ul);
|
||||
}
|
||||
|
||||
async function onForgeRemoveClick(agent, forge, btn) {
|
||||
const r = await themedConfirm({
|
||||
message: `remove ${agent}'s stored token for ${forge.label}? this only deletes the local copy — nothing changes on the remote forge.`,
|
||||
danger: true,
|
||||
confirmLabel: '⊘ remove',
|
||||
});
|
||||
if (!r) return;
|
||||
|
||||
btn.disabled = true;
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'removing…';
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forge-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({ agent, label: forge.label, action: 'remove' }),
|
||||
});
|
||||
if (resp.ok) {
|
||||
loadForgeAccounts(agent);
|
||||
return;
|
||||
}
|
||||
const msg = await readErrorBody(resp);
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast('✗ ' + (msg || ('remove failed (HTTP ' + resp.status + ')')), { type: 'error' });
|
||||
} catch (err) {
|
||||
btn.textContent = orig;
|
||||
btn.disabled = false;
|
||||
themedToast('✗ request failed: ' + String(err), { type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
async function submitForgeAccount(e) {
|
||||
e.preventDefault();
|
||||
const formEl = e.target;
|
||||
const out = $('ef-result');
|
||||
out.className = 'ma-result';
|
||||
out.textContent = '';
|
||||
|
||||
const agent = $('ma-agent').value;
|
||||
if (!agent) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = 'select an agent first.';
|
||||
return;
|
||||
}
|
||||
|
||||
const fd = new FormData(formEl);
|
||||
fd.set('agent', agent);
|
||||
fd.set('action', 'add');
|
||||
|
||||
const btn = formEl.querySelector('button[type="submit"]');
|
||||
const orig = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'storing…';
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/extra-forge-account', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams(fd),
|
||||
});
|
||||
|
||||
if (resp.ok) {
|
||||
let body = {};
|
||||
try { body = await resp.json(); } catch { /* tolerate odd 2xx body */ }
|
||||
if (body.ok) {
|
||||
out.className = 'ma-result ok';
|
||||
out.textContent = '✓ forge account stored.';
|
||||
clearSecrets(formEl);
|
||||
loadForgeAccounts(agent);
|
||||
} else {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ store failed (unexpected response).';
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} else {
|
||||
const msg = await readErrorBody(resp);
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ ' + (msg || ('store failed (HTTP ' + resp.status + ')'));
|
||||
clearSecrets(formEl);
|
||||
}
|
||||
} catch (err) {
|
||||
out.className = 'ma-result err';
|
||||
out.textContent = '✗ request failed: ' + String(err) + ' (the backend endpoint may not be deployed yet).';
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = orig;
|
||||
}
|
||||
}
|
||||
|
||||
// ─── init ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function onAgentChange(agent) {
|
||||
loadAccounts(agent);
|
||||
loadGithubStatus(agent);
|
||||
loadForgeAccounts(agent);
|
||||
}
|
||||
|
||||
async function init() {
|
||||
|
|
@ -358,6 +504,7 @@ async function init() {
|
|||
toggleModeFields();
|
||||
$('ma-form').addEventListener('submit', submitLogin);
|
||||
$('gh-form').addEventListener('submit', submitGithub);
|
||||
$('ef-form').addEventListener('submit', submitForgeAccount);
|
||||
|
||||
createTabStrip(document.getElementById('cred-tabbar'), {
|
||||
defaultId: 'matrix',
|
||||
|
|
|
|||
Loading…
Reference in a new issue