[grade=B] DC-131/132/133 install from any Git host
Codex source gate: urn:ump:iw2tbbe6mssyl5divymmwo42ael65sbfrciztvyowo3zhrbtircq Generated assets gate: urn:ump:hintflviuxfpeidth42ry5fi4lwqsjhkxipzspfmk7vrvfc2cnqq
This commit is contained in:
+165
-1
@@ -6,6 +6,31 @@
|
||||
// Follows the security-center.js modal pattern (injectModal + button in the
|
||||
// top bar) and the weather-modal visual language.
|
||||
|
||||
// DC-133: pure repo-option builder. Remote Gitea fields are UNTRUSTED (a
|
||||
// hostile instance controls full_name/description/url), so every
|
||||
// interpolated value must be HTML-escaped before innerHTML. Exposed on
|
||||
// window so status/tests can pin the escaping with hostile payloads.
|
||||
window.__dc133_buildRepoOptions = function (repos, escFn) {
|
||||
var e = escFn || function (s) {
|
||||
return String(s).replace(/[&<>"']/g, function (c) {
|
||||
return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c];
|
||||
});
|
||||
};
|
||||
return '<option value="">— choose a repo —</option>' +
|
||||
(repos || []).map(function (r) {
|
||||
return '<option value="' + e(r.url) + '" data-id="' + e(r.id) + '">' +
|
||||
e(r.full_name) + (r.description ? ' — ' + e(r.description) : '') + '</option>';
|
||||
}).join('');
|
||||
};
|
||||
|
||||
// Three-state token semantics, exposed for VM tests:
|
||||
// anonymous=true -> explicit empty; typed value -> token; neither -> omitted.
|
||||
window.__dc133_requestToken = function (anonymous, typed) {
|
||||
if (anonymous) return '';
|
||||
var t = String(typed || '').trim();
|
||||
return t || undefined;
|
||||
};
|
||||
|
||||
(function () {
|
||||
injectModal('deploys-modal', `<div id="deploys-modal" class="weather-modal">
|
||||
<div class="weather-modal-content" style="min-width: 860px; max-width: 1200px;">
|
||||
@@ -17,7 +42,7 @@
|
||||
<div class="sec-tabs" style="display:flex;gap:8px;margin-bottom:14px;border-bottom:1px solid var(--border);">
|
||||
<button class="dep-tab active" data-tab="services">Services</button>
|
||||
<button class="dep-tab" data-tab="deploy">Deploy</button>
|
||||
<button class="dep-tab" data-tab="journal">Journal</button>
|
||||
<button class="dep-tab" data-tab="journal">Journal</button><button class="dep-tab" data-tab="install">Install</button>
|
||||
</div>
|
||||
|
||||
<!-- SERVICES TAB -->
|
||||
@@ -47,6 +72,43 @@
|
||||
<div id="dep-journal" class="scroll-container" style="max-height:320px;">Loading…</div>
|
||||
</div>
|
||||
|
||||
<!-- INSTALL TAB (DC-131) -->
|
||||
<div class="dep-panel" data-panel="install" style="display:none;">
|
||||
<div style="display:grid;gap:10px;max-width:640px;">
|
||||
<div style="display:grid;gap:4px;">
|
||||
<span class="modal-subtitle" style="margin:0;">Gitea server (any instance)</span>
|
||||
<div style="display:flex;gap:8px;">
|
||||
<input id="dep-gh-gitea-host" placeholder="git.dashcaddy.net (default)" style="flex:1;" autocomplete="off" spellcheck="false" />
|
||||
<input id="dep-gh-gitea-token" type="password" placeholder="token (private repos)" style="flex:1;" autocomplete="off" spellcheck="false" />
|
||||
</div>
|
||||
<label style="display:flex;gap:6px;align-items:center;font-size:12px;">
|
||||
<input id="dep-gh-anonymous" type="checkbox" />
|
||||
Anonymous — ignore the saved fleet token for this request
|
||||
</label>
|
||||
</div>
|
||||
<label style="display:grid;gap:4px;">
|
||||
<span class="modal-subtitle" style="margin:0;">Pick a repo</span>
|
||||
<select id="dep-gh-gitea" style="min-width:280px;"><option value="">Loading…</option></select>
|
||||
</label>
|
||||
<label style="display:grid;gap:4px;">
|
||||
<span class="modal-subtitle" style="margin:0;">…or paste any repo URL (GitHub, Gitea, or any https git host)</span>
|
||||
<input id="dep-gh-url" placeholder="https://host/owner/repo" style="width:100%;" />
|
||||
</label>
|
||||
<label style="display:grid;gap:4px;">
|
||||
<span class="modal-subtitle" style="margin:0;">Name (used as subdomain; card title falls back to repo name)</span>
|
||||
<input id="dep-gh-service" placeholder="my-app" style="width:220px;" />
|
||||
</label>
|
||||
<label style="display:grid;gap:4px;">
|
||||
<span class="modal-subtitle" style="margin:0;">Optional launch args (space-separated simple tokens, e.g. -text hi)</span>
|
||||
<input id="dep-gh-args" placeholder="-text hello" style="width:100%;" />
|
||||
</label>
|
||||
<p class="modal-subtitle" style="margin:0;">
|
||||
Go repos with a main package install automatically: build, systemd release,
|
||||
Caddy gate (tailnet-only), DNS, verify, dashboard card. Takes ~30-60s.
|
||||
</p>
|
||||
<div><button class="btn btn-primary" id="dep-gh-btn">Install</button></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- OUTPUT -->
|
||||
<pre id="dep-output" class="scroll-container" style="display:none;max-height:260px;margin-top:12px;background:var(--bg-2,#111);padding:10px;border-radius:8px;white-space:pre-wrap;"></pre>
|
||||
|
||||
@@ -136,6 +198,91 @@
|
||||
).join('') + '</tbody></table>';
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function loadGiteaRepos() {
|
||||
const sel = document.getElementById('dep-gh-gitea');
|
||||
const hostEl = document.getElementById('dep-gh-gitea-host');
|
||||
const tokEl = document.getElementById('dep-gh-gitea-token');
|
||||
const anonEl = document.getElementById('dep-gh-anonymous');
|
||||
const payload = {};
|
||||
if (hostEl && hostEl.value.trim()) payload.gitea_url = 'https://' + hostEl.value.trim().replace(/^https?:\/*/, '');
|
||||
// Distinguish omitted (fleet fallback allowed) from explicit anonymous
|
||||
// (empty token preserved on wire). A non-empty user token wins.
|
||||
if (anonEl && anonEl.checked) payload.token = '';
|
||||
else {
|
||||
const t = window.__dc133_requestToken(false, tokEl && tokEl.value);
|
||||
if (t !== undefined) payload.token = t;
|
||||
}
|
||||
try {
|
||||
const { status, body } = await api('/gitea-repos', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!body.success || !(body.repos || []).length) {
|
||||
sel.innerHTML = '<option value="">' + esc(body.error || 'no repos found') + '</option>';
|
||||
return;
|
||||
}
|
||||
// All remote fields are untrusted (hostile Gitea instance or repo
|
||||
// metadata) — build options through the escaping helper (pinned by
|
||||
// status/tests/deploys-install.test.js).
|
||||
sel.innerHTML = window.__dc133_buildRepoOptions(body.repos, esc);
|
||||
} catch (e) {
|
||||
sel.innerHTML = '<option value="">unavailable</option>';
|
||||
}
|
||||
}
|
||||
|
||||
async function runInstall() {
|
||||
const btn = document.getElementById('dep-gh-btn');
|
||||
const repoUrl = document.getElementById('dep-gh-url').value.trim();
|
||||
const service = (document.getElementById('dep-gh-service').value.trim() || '').toLowerCase();
|
||||
const argsRaw = document.getElementById('dep-gh-args').value.trim();
|
||||
if (!repoUrl || !service) { showOutput('Repo URL and name are required.'); return; }
|
||||
const args = argsRaw ? argsRaw.split(/\s+/) : [];
|
||||
setBusy(true, btn, 'Install');
|
||||
showOutput('Installing ' + repoUrl + '\nCloning, building, gating and DNS-ing... (~30-60s)');
|
||||
try {
|
||||
const anonymous = !!(document.getElementById('dep-gh-anonymous') || {}).checked;
|
||||
const typedToken = (document.getElementById('dep-gh-gitea-token') || { value: '' }).value;
|
||||
const requestToken = window.__dc133_requestToken(anonymous, typedToken);
|
||||
const { status, body } = await api('/install', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ repo_url: repoUrl, service, args,
|
||||
token: requestToken }),
|
||||
});
|
||||
if (!body.success) {
|
||||
showOutput((body.output ? body.output + '\n' : '') + 'FAIL ' + (body.error || ('HTTP ' + status)));
|
||||
setBusy(false, btn, 'Install');
|
||||
return;
|
||||
}
|
||||
const svc = body.service || {};
|
||||
const exists = (window.APPS || []).some((a) => a.id === svc.id);
|
||||
if (!exists) {
|
||||
const card = { id: svc.id, name: svc.name || svc.id, url: svc.url, logo: svc.logo, tailscaleOnly: true, isCustom: true };
|
||||
try {
|
||||
await fetch('/dashcaddy-api/api/v1/services', {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(card),
|
||||
});
|
||||
window.APPS.push(card);
|
||||
if (typeof window.renderApps === 'function') window.renderApps();
|
||||
if (typeof window.renderGrid === 'function') window.renderGrid();
|
||||
} catch (e) { /* card registration best-effort */ }
|
||||
}
|
||||
showOutput('INSTALLED ' + (svc.name || svc.id) + ' in ' + (svc.deploy_seconds || '?') + 's' +
|
||||
'\nCard: ' + (svc.url || '') +
|
||||
'\n' + (body.output || '').slice(-800));
|
||||
loadServices();
|
||||
} catch (e) {
|
||||
showOutput('install error: ' + e.message);
|
||||
}
|
||||
setBusy(false, btn, 'Install');
|
||||
}
|
||||
|
||||
function setBusy(busy, btn, label) {
|
||||
if (!btn) return;
|
||||
btn.disabled = busy;
|
||||
@@ -202,6 +349,7 @@
|
||||
loadServices();
|
||||
loadRepos();
|
||||
loadJournal('');
|
||||
loadGiteaRepos();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -221,6 +369,22 @@
|
||||
});
|
||||
|
||||
$('dep-deploy-btn').addEventListener('click', runDeploy);
|
||||
$('dep-gh-btn').addEventListener('click', runInstall);
|
||||
['dep-gh-gitea-host', 'dep-gh-gitea-token'].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.addEventListener('change', () => loadGiteaRepos());
|
||||
});
|
||||
document.getElementById('dep-gh-gitea').addEventListener('change', (e) => {
|
||||
const v = e.target.value;
|
||||
if (v) {
|
||||
document.getElementById('dep-gh-url').value = v;
|
||||
const opt = e.target.selectedOptions[0];
|
||||
const id = opt && opt.dataset ? opt.dataset.id : '';
|
||||
const svc = document.getElementById('dep-gh-service');
|
||||
if (id && !svc.value) svc.value = id;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
$('dep-journal-btn').addEventListener('click', () => loadJournal($('dep-journal-name').value.trim()));
|
||||
$('dep-status-btn').addEventListener('click', () => {
|
||||
|
||||
Reference in New Issue
Block a user