(function () {
"use strict";
var app = document.getElementById("app");
var i18n = {};
var currentLocale = "en";
var localeOptions = [];
function t(key, args) {
var text = i18n[key] || key.replace(/^ui\./, "");
if (args) {
for (var i = 0; i < args.length; i++) {
text = text.replace("{}", args[i]);
}
}
return text;
}
function renderLocalePicker() {
if (localeOptions.length === 0) return "";
var html = '<select id="locale-picker" class="locale-picker">';
localeOptions.forEach(function (loc) {
var sel = loc.code === currentLocale ? " selected" : "";
html += '<option value="' + esc(loc.code) + '"' + sel + '>' + esc(loc.label) + '</option>';
});
html += '</select>';
return html;
}
function setupLocalePicker() {
var picker = document.getElementById("locale-picker");
if (!picker) return;
picker.addEventListener("change", function () {
currentLocale = picker.value;
reloadWithLocale();
});
}
var RTL_LOCALES = ["ar", "he", "fa", "ur"];
function isRtl(locale) {
return RTL_LOCALES.some(function (r) { return locale === r || locale.startsWith(r + "-"); });
}
function applyDirection() {
document.documentElement.dir = isRtl(currentLocale) ? "rtl" : "ltr";
}
function reloadWithLocale() {
applyDirection();
fetch("/api/providers?locale=" + encodeURIComponent(currentLocale))
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.i18n) i18n = data.i18n;
state.providerForms = {};
(data.provider_forms || []).forEach(function (pf) {
state.providerForms[pf.provider_id] = pf;
});
state.sharedQuestions = data.shared_questions || [];
render();
});
}
function makeScope(tenant, env, team) {
var answers = {};
state.providers.forEach(function (p) {
answers[p.provider_id] = {};
var form = state.providerForms[p.provider_id];
if (form) {
form.questions.forEach(function (q) {
if (q.saved_value) answers[p.provider_id][q.id] = q.saved_value;
});
}
});
return {
tenant: tenant || "demo",
env: env || "dev",
team: team || "",
answers: answers,
sharedAnswers: {},
providersDone: {},
sharedAnswersDone: false,
executed: false,
};
}
var state = {
phase: "loading",
providers: [],
sharedQuestions: [],
providerForms: {},
bundlePath: "",
detectedTenant: null,
scopes: [],
currentScopeIdx: -1,
currentProvider: 0,
result: null,
};
function cs() { return state.scopes[state.currentScopeIdx]; }
function render() {
switch (state.phase) {
case "loading": renderLoading(); break;
case "dashboard": renderDashboard(); break;
case "scope-edit": renderScopeEdit(); break;
case "providers": renderProviders(); break;
case "shared": renderForm(state.sharedQuestions, t("ui.shared.title"), t("ui.shared.description"), null, submitShared); break;
case "provider-form": renderProviderForm(); break;
case "review": renderReview(); break;
case "executing": renderExecuting(); break;
case "result": renderResult(); break;
case "export": renderExport(); break;
}
}
function renderLoading() {
app.innerHTML =
'<div class="fade-in center-msg">' +
'<div class="spinner"></div>' +
'<p class="executing-text">' + esc(t("ui.discovering")) + '</p>' +
'<p class="executing-sub">' + esc(t("ui.discovering_sub")) + '</p>' +
'</div>';
applyDirection();
fetch("/api/locales")
.then(function (r) { return r.json(); })
.then(function (locData) {
localeOptions = locData.locales || [];
currentLocale = locData.current || "en";
return fetch("/api/providers?locale=" + encodeURIComponent(currentLocale));
})
.then(function (r) { return r.json(); })
.then(function (data) {
if (data.i18n) i18n = data.i18n;
state.bundlePath = data.bundle_path || "";
state.providers = data.providers || [];
state.providerForms = {};
(data.provider_forms || []).forEach(function (pf) {
state.providerForms[pf.provider_id] = pf;
});
state.sharedQuestions = data.shared_questions || [];
if (state.providers.length === 0) {
app.innerHTML =
'<div class="fade-in center-msg">' +
'<p class="executing-text">' + esc(t("ui.no_providers")) + '</p>' +
'<p class="executing-sub">' + esc(t("ui.nothing_to_configure")) + '</p>' +
'<br><button class="btn btn-ghost" id="btn-close-empty">' + esc(t("ui.close")) + '</button>' +
'</div>';
document.getElementById("btn-close-empty").addEventListener("click", shutdown);
return;
}
return Promise.all([
fetch("/api/existing-scopes").then(function (r) { return r.json(); }),
fetch("/api/scope").then(function (r) { return r.json(); }),
]);
})
.then(function (results) {
var existingData = results[0];
var scopeData = results[1];
state.detectedTenant = scopeData.detected_tenant || null;
var existingScopes = existingData.scopes || [];
if (existingScopes.length > 0) {
state.scopes = existingScopes.map(function (es) {
var s = makeScope(es.tenant || "demo", es.env || "dev", es.team || "");
if (es.answers) {
Object.keys(es.answers).forEach(function (pid) {
if (typeof es.answers[pid] === "object") {
s.answers[pid] = es.answers[pid];
}
});
}
if (es.providers_done) {
es.providers_done.forEach(function (pid) { s.providersDone[pid] = true; });
}
return s;
});
state.phase = "dashboard";
} else if (state.scopes.length === 0) {
state.scopes.push(makeScope(
scopeData.tenant || "demo",
scopeData.env || "dev",
scopeData.team || ""
));
state.currentScopeIdx = 0;
state.currentProvider = 0;
state.phase = "scope-edit";
} else {
state.phase = "dashboard";
}
render();
})
.catch(function (err) {
app.innerHTML =
'<div class="fade-in center-msg">' +
'<p class="executing-text">Failed to discover providers</p>' +
'<p class="executing-sub">' + esc(err.message) + '</p>' +
'</div>';
});
}
function renderDashboard() {
var html =
'<div class="fade-in">' +
'<div class="brand">' +
'<div class="brand-icon">' +
'<svg width="32" height="32" viewBox="0 0 32 32" fill="none"><rect width="32" height="32" rx="8" fill="#25c39e"/><path d="M10 16.5L14 20.5L22 12.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</div>' +
'<h1 class="brand-title">' + esc(t("ui.title")) + '</h1>' +
'<p class="brand-desc">' + esc(t("ui.dashboard.description", [String(state.providers.length), state.bundlePath])) + '</p>' +
renderLocalePicker() +
'</div>';
html += '<div class="provider-list">';
state.scopes.forEach(function (scope, idx) {
var configured = Object.keys(scope.providersDone).length;
var total = state.providers.length;
var statusClass = scope.executed ? "done" : (configured === total && total > 0 ? "done" : "pending");
var statusText = scope.executed ? t("ui.dashboard.executed") : (configured + "/" + total + " " + t("ui.dashboard.configured"));
html +=
'<div class="provider-card scope-card" data-idx="' + idx + '">' +
'<div class="prov-icon" style="background:#6366f1">' + esc(scope.tenant.charAt(0).toUpperCase()) + '</div>' +
'<div style="flex:1">' +
'<div class="prov-name">' + esc(scope.tenant) + '</div>' +
'<div class="prov-domain">' + esc(scope.env) + (scope.team ? " / " + esc(scope.team) : "") + '</div>' +
'</div>' +
'<span class="prov-badge ' + statusClass + '">' + esc(statusText) + '</span>' +
'<button class="btn btn-ghost btn-sm scope-delete" data-idx="' + idx + '" title="' + esc(t("ui.dashboard.delete")) + '">' +
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18M8 6V4h8v2M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6"/></svg>' +
'</button>' +
'</div>';
});
html += '</div>';
html +=
'<div id="import-zone" class="import-zone">' +
'<input type="file" id="import-file" accept=".json,.yaml,.yml" style="display:none" />' +
'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>' +
' ' + esc(t("ui.import.dropzone")) +
'</div>';
html +=
'<div style="display:flex;gap:.5rem;flex-wrap:wrap">' +
'<button class="btn btn-secondary" id="btn-add-scope" style="flex:1">' + esc(t("ui.dashboard.add_scope")) + '</button>' +
'<button class="btn btn-secondary" id="btn-export" style="flex:1">' + esc(t("ui.dashboard.export")) + '</button>' +
'</div>' +
'<div style="text-align:center;margin-top:.75rem"><button class="btn btn-ghost" id="btn-close-dash">' + esc(t("ui.close")) + '</button></div>' +
'</div>';
app.innerHTML = html;
setupLocalePicker();
app.querySelectorAll(".scope-card").forEach(function (card) {
card.addEventListener("click", function (e) {
if (e.target.closest(".scope-delete")) return;
var idx = parseInt(card.getAttribute("data-idx"), 10);
state.currentScopeIdx = idx;
state.currentProvider = 0;
state.phase = "scope-edit";
render();
});
});
app.querySelectorAll(".scope-delete").forEach(function (btn) {
btn.addEventListener("click", function (e) {
e.stopPropagation();
var idx = parseInt(btn.getAttribute("data-idx"), 10);
if (state.scopes.length <= 1) return; state.scopes.splice(idx, 1);
render();
});
});
document.getElementById("btn-add-scope").addEventListener("click", function () {
var newScope = makeScope(
state.detectedTenant || "demo",
"dev",
""
);
state.scopes.push(newScope);
state.currentScopeIdx = state.scopes.length - 1;
state.currentProvider = 0;
state.phase = "scope-edit";
render();
});
document.getElementById("btn-export").addEventListener("click", function () {
state.phase = "export";
render();
});
var importZone = document.getElementById("import-zone");
var importFile = document.getElementById("import-file");
importZone.addEventListener("click", function () { importFile.click(); });
importFile.addEventListener("change", function () {
if (importFile.files.length > 0) handleImportFile(importFile.files[0]);
});
importZone.addEventListener("dragover", function (e) { e.preventDefault(); importZone.classList.add("drag-over"); });
importZone.addEventListener("dragleave", function () { importZone.classList.remove("drag-over"); });
importZone.addEventListener("drop", function (e) {
e.preventDefault();
importZone.classList.remove("drag-over");
if (e.dataTransfer.files.length > 0) handleImportFile(e.dataTransfer.files[0]);
});
document.getElementById("btn-close-dash").addEventListener("click", shutdown);
}
function handleImportFile(file) {
var reader = new FileReader();
reader.onload = function () {
try {
var doc = JSON.parse(reader.result);
processImportedDoc(doc);
} catch (e) {
alert(t("ui.import.parse_error") + ": " + e.message);
}
};
reader.readAsText(file);
}
function processImportedDoc(doc) {
if (docHasEncrypted(doc)) {
var key = prompt(t("ui.import.password_prompt"));
if (!key) return;
fetch("/api/decrypt", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ doc: doc, key: key }),
})
.then(function (r) { return r.json(); })
.then(function (res) {
if (res.ok) {
importScopesFromDoc(res.doc);
} else {
alert(t("ui.import.decrypt_failed") + ": " + (res.error || "unknown error"));
}
})
.catch(function (err) { alert(t("ui.import.decrypt_failed") + ": " + err.message); });
} else {
importScopesFromDoc(doc);
}
}
function docHasEncrypted(val) {
if (!val || typeof val !== "object") return false;
if (val.__greentic_encrypted__) return true;
var keys = Array.isArray(val) ? val : Object.values(val);
return keys.some(function (v) { return docHasEncrypted(v); });
}
function importScopesFromDoc(doc) {
var imported = [];
if (doc.scopes && Array.isArray(doc.scopes)) {
doc.scopes.forEach(function (s) { imported.push(scopeFromAnswerDoc(s)); });
}
else if (doc.setup_answers && typeof doc.setup_answers === "object") {
imported.push(scopeFromAnswerDoc(doc));
}
else if (typeof doc === "object" && !doc.greentic_setup_version) {
var s = makeScope("demo", "dev", "");
Object.keys(doc).forEach(function (k) {
if (typeof doc[k] === "object" && !Array.isArray(doc[k])) {
s.answers[k] = doc[k];
}
});
imported.push(s);
}
if (imported.length === 0) {
alert(t("ui.import.no_scopes"));
return;
}
state.scopes = imported;
state.phase = "dashboard";
render();
}
function scopeFromAnswerDoc(doc) {
var s = makeScope(
doc.tenant || "demo",
doc.env || "dev",
doc.team || ""
);
if (doc.setup_answers && typeof doc.setup_answers === "object") {
Object.keys(doc.setup_answers).forEach(function (pid) {
if (typeof doc.setup_answers[pid] === "object") {
s.answers[pid] = doc.setup_answers[pid];
var keys = Object.keys(doc.setup_answers[pid]);
if (keys.length > 0) s.providersDone[pid] = true;
}
});
}
return s;
}
function renderScopeEdit() {
var scope = cs();
var html =
'<div class="fade-in">' +
'<div class="step-header">' +
'<button class="btn btn-ghost btn-sm btn-back" id="btn-back">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>' +
' ' + esc(t("ui.back")) +
'</button>' +
'</div>' +
'<div class="card">' +
'<div class="card-header">' +
'<h2 class="card-title">' + esc(t("ui.scope.title")) + '</h2>' +
'<p class="card-desc">' + esc(t("ui.scope.hint")) + '</p>' +
'</div>' +
'<div class="card-content"><div class="form-fields">';
html +=
'<div class="field">' +
'<label class="field-label" for="f-scope-tenant">' + esc(t("ui.scope.tenant")) + '<span class="required">*</span></label>' +
'<input type="text" id="f-scope-tenant" value="' + esc(scope.tenant) + '" />';
if (state.detectedTenant) {
html += '<p class="field-help">' + esc(t("ui.scope.detected_tenant", [state.detectedTenant])) + '</p>';
}
html += '</div>';
html +=
'<div class="field">' +
'<label class="field-label" for="f-scope-env">' + esc(t("ui.scope.env")) + '<span class="required">*</span></label>' +
'<select id="f-scope-env">' +
'<option value="dev"' + (scope.env === "dev" ? " selected" : "") + '>dev</option>' +
'<option value="local"' + (scope.env === "local" ? " selected" : "") + '>local</option>' +
'<option value="test"' + (scope.env === "test" ? " selected" : "") + '>test</option>' +
'<option value="staging"' + (scope.env === "staging" ? " selected" : "") + '>staging</option>' +
'<option value="prod"' + (scope.env === "prod" ? " selected" : "") + '>prod</option>' +
'</select>' +
'<p class="field-help">' + esc(t("ui.scope.env_help")) + '</p>' +
'</div>';
html +=
'<div class="field">' +
'<label class="field-label" for="f-scope-team">' + esc(t("ui.scope.team")) + '</label>' +
'<input type="text" id="f-scope-team" value="' + esc(scope.team) + '" placeholder="default" />' +
'<p class="field-help">' + esc(t("ui.scope.team_help")) + '</p>' +
'</div>';
html +=
'</div></div>' +
'<div class="card-footer">' +
'<button class="btn btn-primary btn-lg" id="btn-scope-continue" style="width:100%">' + esc(t("ui.continue")) + '</button>' +
'</div>' +
'</div>' +
'</div>';
app.innerHTML = html;
document.getElementById("btn-back").addEventListener("click", function () {
state.phase = "dashboard";
render();
});
document.getElementById("btn-scope-continue").addEventListener("click", function () {
var tenantEl = document.getElementById("f-scope-tenant");
var tenant = tenantEl.value.trim();
if (!tenant) { tenantEl.classList.add("input-error"); tenantEl.focus(); return; }
scope.tenant = tenant;
scope.env = document.getElementById("f-scope-env").value;
scope.team = document.getElementById("f-scope-team").value.trim();
state.phase = "providers";
render();
});
}
function renderProviders() {
var scope = cs();
var allDone = state.providers.every(function (p) { return scope.providersDone[p.provider_id]; });
var html =
'<div class="fade-in">' +
'<div class="brand">' +
'<div class="brand-icon">' +
'<svg width="32" height="32" viewBox="0 0 32 32" fill="none"><rect width="32" height="32" rx="8" fill="#25c39e"/><path d="M10 16.5L14 20.5L22 12.5" stroke="white" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/></svg>' +
'</div>' +
'<h1 class="brand-title">' + esc(t("ui.title")) + '</h1>' +
'<p class="brand-desc">' + esc(t("ui.description", [String(state.providers.length), state.bundlePath])) + '</p>' +
'<p class="brand-desc" style="font-size:.8rem;opacity:.7">tenant=' + esc(scope.tenant) + ' env=' + esc(scope.env) + (scope.team ? ' team=' + esc(scope.team) : '') + '</p>' +
'</div>' +
'<div class="provider-list">';
state.providers.forEach(function (p) {
var done = scope.providersDone[p.provider_id];
var form = state.providerForms[p.provider_id];
var qCount = form ? form.questions.length : 0;
var displayName = formatProviderName(p);
html +=
'<div class="provider-card">' +
'<div class="prov-icon">' + esc(displayName.charAt(0)) + '</div>' +
'<div>' +
'<div class="prov-name">' + esc(displayName) + '</div>' +
'<div class="prov-domain">' + esc(p.domain) + ' · ' + qCount + ' ' + esc(t("ui.questions")) + '</div>' +
'</div>' +
'<span class="prov-badge ' + (done ? 'done' : 'pending') + '">' + (done ? esc(t("ui.done")) : esc(t("ui.pending"))) + '</span>' +
'</div>';
});
html += '</div>';
if (state.sharedQuestions.length > 0 && !scope.sharedAnswersDone) {
html += '<button class="btn btn-primary btn-lg" id="btn-start" style="width:100%">' + esc(t("ui.start_config")) + '</button>';
} else if (!allDone) {
var nextIdx = state.providers.findIndex(function (p) { return !scope.providersDone[p.provider_id]; });
html += '<button class="btn btn-primary btn-lg" id="btn-next-prov" data-idx="' + nextIdx + '" style="width:100%">' + esc(t("ui.configure", [formatProviderName(state.providers[nextIdx])])) + '</button>';
} else {
html += '<button class="btn btn-primary btn-lg" id="btn-review" style="width:100%">' + esc(t("ui.review_execute")) + '</button>';
}
html +=
'<div style="text-align:center;margin-top:.75rem;display:flex;justify-content:center;gap:.5rem">' +
'<button class="btn btn-ghost" id="btn-back-scope">' + esc(t("ui.back")) + '</button>' +
'</div></div>';
app.innerHTML = html;
setupLocalePicker();
var startBtn = document.getElementById("btn-start");
if (startBtn) startBtn.addEventListener("click", function () { state.phase = "shared"; render(); });
var nextBtn = document.getElementById("btn-next-prov");
if (nextBtn) nextBtn.addEventListener("click", function () {
state.currentProvider = parseInt(nextBtn.getAttribute("data-idx"), 10);
state.phase = "provider-form";
render();
});
var reviewBtn = document.getElementById("btn-review");
if (reviewBtn) reviewBtn.addEventListener("click", function () { state.phase = "review"; render(); });
document.getElementById("btn-back-scope").addEventListener("click", function () {
state.phase = "scope-edit";
render();
});
}
function renderForm(questions, title, desc, backPhase, onSubmit, backFn) {
var html =
'<div class="fade-in">' +
'<div class="step-header">' +
'<button class="btn btn-ghost btn-sm btn-back" id="btn-back">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>' +
' ' + esc(t("ui.back")) +
'</button>' +
'</div>' +
'<div class="card">' +
'<div class="card-header">' +
'<h2 class="card-title">' + esc(title) + '</h2>' +
'<p class="card-desc">' + esc(desc) + '</p>' +
'</div>' +
'<div class="card-content"><div id="form-area" class="form-fields">';
var currentGroup = null;
questions.forEach(function (q) {
if (q.group && q.group !== currentGroup) {
if (currentGroup !== null) html += '</div>';
currentGroup = q.group;
html += '<div class="form-group"><h4 class="form-group-title">' + esc(q.group) + '</h4>';
} else if (!q.group && currentGroup !== null) {
html += '</div>';
currentGroup = null;
}
html += renderQuestion(q);
});
if (currentGroup !== null) html += '</div>';
html +=
'</div></div>' +
'<div class="card-footer card-footer-split">' +
'<button class="btn btn-secondary" id="btn-prev">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>' +
' ' + esc(t("ui.back")) +
'</button>' +
'<button class="btn btn-primary" id="btn-submit">' + esc(t("ui.continue")) + '</button>' +
'</div>' +
'</div>' +
'</div>';
app.innerHTML = html;
restoreFormValues(questions);
setupVisibility(questions);
var goBack = backFn || function () { state.phase = backPhase || "providers"; render(); };
document.getElementById("btn-back").addEventListener("click", function () { collectFormValues(questions); goBack(); });
document.getElementById("btn-prev").addEventListener("click", function () { collectFormValues(questions); goBack(); });
document.getElementById("btn-submit").addEventListener("click", function () {
if (!validateForm(questions)) return;
collectFormValues(questions);
onSubmit();
});
}
function renderQuestion(q) {
var visAttr = "";
if (q.visible_if) {
visAttr = ' data-vis-field="' + esc(q.visible_if.field) + '" data-vis-eq="' + esc(q.visible_if.eq || "") + '"';
}
var html = '<div class="field" id="field-' + esc(q.id) + '"' + visAttr + '>';
if (q.kind === "Boolean") {
html +=
'<div class="field-row">' +
'<label class="field-label" for="f-' + esc(q.id) + '">' + esc(q.title) + '</label>' +
'<label class="switch"><input type="checkbox" id="f-' + esc(q.id) + '" name="' + esc(q.id) + '" /><span class="switch-slider"></span></label>' +
'</div>';
} else {
html += '<label class="field-label" for="f-' + esc(q.id) + '">' + esc(q.title);
if (q.required) html += '<span class="required">*</span>';
html += '</label>';
var ph = q.placeholder || q.default_value || "";
if (q.choices && q.choices.length > 0) {
html += '<select id="f-' + esc(q.id) + '" name="' + esc(q.id) + '">';
q.choices.forEach(function (c) { html += '<option value="' + esc(c) + '">' + esc(c) + '</option>'; });
html += '</select>';
} else if (q.secret) {
html += '<input type="password" id="f-' + esc(q.id) + '" name="' + esc(q.id) + '"' + (ph ? ' placeholder="' + esc(ph) + '"' : '') + ' />';
} else {
html += '<input type="text" id="f-' + esc(q.id) + '" name="' + esc(q.id) + '"' + (ph ? ' placeholder="' + esc(ph) + '"' : '') + ' />';
}
if (q.help || q.docs_url) {
html += '<div class="field-meta">';
if (q.help) html += '<p class="field-help">' + esc(q.help) + '</p>';
if (q.docs_url) html += '<a class="field-docs" href="' + esc(q.docs_url) + '" target="_blank" rel="noopener">Setup Guide ↗</a>';
html += '</div>';
}
}
html += '</div>';
return html;
}
function restoreFormValues(questions) {
var scope = cs();
var store = state.phase === "shared" ? scope.sharedAnswers :
(scope.answers[state.providers[state.currentProvider].provider_id] || {});
questions.forEach(function (q) {
var el = document.getElementById("f-" + q.id);
if (!el) return;
var val = store[q.id];
var effective = val !== undefined ? val : (q.saved_value || undefined);
if (effective !== undefined) {
if (q.kind === "Boolean") { el.checked = effective === true || effective === "true"; }
else { el.value = effective; }
} else if (q.default_value && q.kind !== "Boolean") { el.value = q.default_value; }
else if (q.kind === "Boolean" && q.default_value) { el.checked = q.default_value === "true" || q.default_value === true; }
});
}
function collectFormValues(questions) {
var scope = cs();
var store = state.phase === "shared" ? scope.sharedAnswers :
(scope.answers[state.providers[state.currentProvider].provider_id] || {});
questions.forEach(function (q) {
var el = document.getElementById("f-" + q.id);
if (!el) return;
var fieldDiv = document.getElementById("field-" + q.id);
var isHidden = fieldDiv && fieldDiv.style.display === "none";
if (q.kind === "Boolean") { store[q.id] = el.checked ? "true" : "false"; }
else if (!isHidden) { var val = el.value.trim(); if (val) store[q.id] = val; }
});
if (state.phase === "shared") { scope.sharedAnswers = store; }
else { scope.answers[state.providers[state.currentProvider].provider_id] = store; }
}
function setupVisibility(questions) {
var depFields = {};
questions.forEach(function (q) { if (q.visible_if) depFields[q.visible_if.field] = true; });
Object.keys(depFields).forEach(function (fid) {
var el = document.getElementById("f-" + fid);
if (el) { var h = function () { evaluateVisibility(); }; el.addEventListener("change", h); el.addEventListener("input", h); }
});
evaluateVisibility();
}
function evaluateVisibility() {
app.querySelectorAll("[data-vis-field]").forEach(function (group) {
var field = group.getAttribute("data-vis-field");
var eqVal = group.getAttribute("data-vis-eq");
var el = document.getElementById("f-" + field);
if (!el) { group.style.display = "none"; return; }
var current = el.type === "checkbox" ? (el.checked ? "true" : "false") : el.value;
group.style.display = current === eqVal ? "" : "none";
});
}
function validateForm(questions) {
clearErrors();
var firstErr = null;
questions.forEach(function (q) {
if (q.kind === "Boolean") return;
var group = document.getElementById("field-" + q.id);
if (group && group.style.display === "none") return;
var el = document.getElementById("f-" + q.id);
if (!el) return;
if (q.required && !el.value.trim() && !q.default_value) {
showFieldError(el, t("ui.field.required", [q.title]));
if (!firstErr) firstErr = el;
}
});
if (firstErr) { firstErr.focus(); return false; }
return true;
}
function clearErrors() {
app.querySelectorAll(".field-error").forEach(function (e) { e.remove(); });
app.querySelectorAll(".input-error").forEach(function (e) { e.classList.remove("input-error"); });
}
function showFieldError(el, msg) {
el.classList.add("input-error");
var err = document.createElement("p");
err.className = "field-error";
err.textContent = msg;
el.parentElement.appendChild(err);
}
function submitShared() {
var scope = cs();
scope.sharedAnswersDone = true;
state.providers.forEach(function (p) {
var store = scope.answers[p.provider_id] || {};
Object.keys(scope.sharedAnswers).forEach(function (k) {
if (!store[k]) store[k] = scope.sharedAnswers[k];
});
scope.answers[p.provider_id] = store;
});
state.currentProvider = 0;
state.phase = "provider-form";
render();
}
function renderProviderForm() {
var scope = cs();
var p = state.providers[state.currentProvider];
var form = state.providerForms[p.provider_id];
if (!form || form.questions.length === 0) {
scope.providersDone[p.provider_id] = true;
advanceProvider();
return;
}
var backFn = function () {
if (state.currentProvider > 0) { state.currentProvider--; state.phase = "provider-form"; }
else if (state.sharedQuestions.length > 0) { state.phase = "shared"; }
else { state.phase = "providers"; }
render();
};
renderForm(form.questions, form.title || formatProviderName(p), t("ui.provider.configure", [formatProviderName(p)]), null, function () {
scope.providersDone[p.provider_id] = true;
advanceProvider();
}, backFn);
}
function advanceProvider() {
var scope = cs();
var next = state.providers.findIndex(function (p) { return !scope.providersDone[p.provider_id]; });
if (next >= 0) { state.currentProvider = next; state.phase = "provider-form"; }
else { state.phase = "review"; }
render();
}
function renderReview() {
var scope = cs();
var html =
'<div class="fade-in">' +
'<div class="step-header">' +
'<button class="btn btn-ghost btn-sm btn-back" id="btn-back-review">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>' +
' ' + esc(t("ui.back")) +
'</button>' +
'</div>' +
'<div class="card">' +
'<div class="card-header">' +
'<h2 class="card-title">' + esc(t("ui.review.title")) + '</h2>' +
'<p class="card-desc">' + esc(t("ui.review.description")) + '</p>' +
'</div>' +
'<div class="card-content">';
html +=
'<div class="review-group"><h4 class="review-group-title">' + esc(t("ui.scope.title")) + '</h4>' +
'<div class="review-item"><span class="review-key">tenant</span><span class="review-val">' + esc(scope.tenant) + '</span></div>' +
'<div class="review-item"><span class="review-key">env</span><span class="review-val">' + esc(scope.env) + '</span></div>' +
'<div class="review-item"><span class="review-key">team</span><span class="review-val">' + esc(scope.team || "default") + '</span></div>' +
'</div>';
state.providers.forEach(function (p) {
var answers = scope.answers[p.provider_id] || {};
var form = state.providerForms[p.provider_id];
var keys = Object.keys(answers);
if (keys.length === 0) return;
html += '<div class="review-group"><h4 class="review-group-title">' + esc(formatProviderName(p)) + '</h4>';
keys.forEach(function (k) {
var val = answers[k];
var isSecret = form && form.questions.some(function (q) { return q.id === k && q.secret; });
var display = typeof val === "boolean" ? (val ? t("ui.review.yes") : t("ui.review.no")) : (isSecret && val ? t("ui.review.secret_mask") : String(val || ""));
if (!display) return;
html += '<div class="review-item"><span class="review-key">' + esc(k) + '</span><span class="review-val' + (isSecret ? ' secret' : '') + '">' + esc(display) + '</span></div>';
});
html += '</div>';
});
html +=
'</div>' +
'<div class="card-footer">' +
'<button class="btn btn-primary btn-lg" id="btn-execute">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>' +
' ' + esc(t("ui.execute_setup")) +
'</button>' +
'</div>' +
'</div>' +
'</div>';
app.innerHTML = html;
document.getElementById("btn-back-review").addEventListener("click", function () { state.phase = "providers"; render(); });
document.getElementById("btn-execute").addEventListener("click", executeSetup);
}
function executeSetup() {
var scope = cs();
state.phase = "executing";
render();
var payload = {
answers: scope.answers,
tenant: scope.tenant,
env: scope.env,
};
if (scope.team) payload.team = scope.team;
fetch("/api/execute", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
})
.then(function (r) { return r.json(); })
.then(function (result) {
scope.executed = true;
state.result = result;
state.phase = "result";
render();
})
.catch(function (err) {
state.result = { success: false, stdout: "", stderr: err.message };
state.phase = "result";
render();
});
}
function renderExecuting() {
app.innerHTML =
'<div class="fade-in center-msg">' +
'<div class="spinner"></div>' +
'<p class="executing-text">' + esc(t("ui.executing.title")) + '</p>' +
'<p class="executing-sub">' + esc(t("ui.executing.sub")) + '</p>' +
'</div>';
}
function renderResult() {
var r = state.result;
var ok = r && r.success;
var html =
'<div class="fade-in"><div class="card">' +
'<div class="card-header center">' +
'<div class="result-icon ' + (ok ? "result-ok" : "result-err") + '">' +
(ok ?
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><path d="m9 11 3 3L22 4"/></svg>' :
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>'
) +
'</div>' +
'<h2 class="card-title">' + (ok ? esc(t("ui.result.success.title")) : esc(t("ui.result.fail.title"))) + '</h2>' +
'<p class="card-desc">' + (ok ? esc(t("ui.result.success.description")) : esc(t("ui.result.fail.description"))) + '</p>' +
'</div>' +
'<div class="card-content">';
if (r && r.manual_steps && r.manual_steps.length > 0) {
html += '<div class="output-section"><h4 class="output-title" style="color:#f59e0b">' + esc(t("ui.result.manual_steps")) + '</h4>';
r.manual_steps.forEach(function (instr) {
html += '<div style="margin-bottom:.75rem;padding:.75rem 1rem;background:rgba(245,158,11,.06);border:1px solid rgba(245,158,11,.2);border-radius:calc(var(--radius) - 2px)">';
html += '<div style="font-size:.8125rem;font-weight:600;color:#f59e0b;margin-bottom:.375rem">' + esc(instr.provider_name) + '</div>';
html += '<ol style="margin:0;padding-left:1.25rem;font-size:.8rem;color:#d4d4d8;line-height:1.7">';
instr.steps.forEach(function (step) { html += '<li>' + esc(step.replace(/^\d+\.\s*/, '')) + '</li>'; });
html += '</ol></div>';
});
html += '</div>';
}
if (r && r.stdout) html += '<div class="output-section"><h4 class="output-title">' + esc(t("ui.result.output")) + '</h4><pre class="output-pre">' + esc(r.stdout) + '</pre></div>';
if (r && r.stderr) html += '<div class="output-section"><h4 class="output-title">' + esc(t("ui.result.log")) + '</h4><pre class="output-pre stderr">' + esc(r.stderr) + '</pre></div>';
html +=
'</div>' +
'<div class="card-footer card-footer-split">' +
'<button class="btn btn-secondary" id="btn-back-dash">' + esc(t("ui.dashboard.back")) + '</button>' +
'<button class="btn btn-ghost" id="btn-close">' + esc(t("ui.close")) + '</button>' +
'</div>' +
'</div></div>';
app.innerHTML = html;
document.getElementById("btn-back-dash").addEventListener("click", function () { state.phase = "dashboard"; render(); });
document.getElementById("btn-close").addEventListener("click", shutdown);
}
function renderExport() {
var html =
'<div class="fade-in">' +
'<div class="step-header">' +
'<button class="btn btn-ghost btn-sm btn-back" id="btn-back-export">' +
'<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>' +
' ' + esc(t("ui.back")) +
'</button>' +
'</div>' +
'<div class="card">' +
'<div class="card-header">' +
'<h2 class="card-title">' + esc(t("ui.export.title")) + '</h2>' +
'<p class="card-desc">' + esc(t("ui.export.description")) + '</p>' +
'</div>' +
'<div class="card-content"><div class="form-fields">' +
'<div class="field">' +
'<label class="field-label" for="f-export-key">' + esc(t("ui.export.password")) + '</label>' +
'<input type="password" id="f-export-key" placeholder="' + esc(t("ui.export.password_hint")) + '" />' +
'<p class="field-help">' + esc(t("ui.export.password_help")) + '</p>' +
'</div>' +
'</div></div>' +
'<div class="card-footer">' +
'<button class="btn btn-primary btn-lg" id="btn-do-export" style="width:100%">' + esc(t("ui.export.download")) + '</button>' +
'</div>' +
'</div>' +
'</div>';
app.innerHTML = html;
document.getElementById("btn-back-export").addEventListener("click", function () { state.phase = "dashboard"; render(); });
document.getElementById("btn-do-export").addEventListener("click", function () {
var key = document.getElementById("f-export-key").value.trim() || null;
var scopes = state.scopes.map(function (s) {
return { tenant: s.tenant, team: s.team || null, env: s.env, answers: s.answers };
});
fetch("/api/export", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ scopes: scopes, key: key }),
})
.then(function (r) { return r.json(); })
.then(function (doc) {
var blob = new Blob([JSON.stringify(doc, null, 2)], { type: "application/json" });
var a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = "setup-answers.json";
a.click();
URL.revokeObjectURL(a.href);
})
.catch(function (err) { alert("Export failed: " + err.message); });
});
}
function shutdown() {
fetch("/api/shutdown", { method: "POST" });
app.innerHTML = '<div class="fade-in center-msg"><p class="executing-text">' + esc(t("ui.result.closed")) + '</p><p class="executing-sub">' + esc(t("ui.result.closed_sub")) + '</p></div>';
}
function esc(str) {
var d = document.createElement("div");
d.textContent = str || "";
return d.innerHTML;
}
function formatProviderName(provider) {
if (typeof provider === "object" && provider.display_name) return provider.display_name;
return typeof provider === "object" ? provider.provider_id : provider;
}
render();
})();