551 lines
19 KiB
HTML
551 lines
19 KiB
HTML
<!doctype html>
|
|
<html lang="it">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Installazione Fiscality</title>
|
|
<style>
|
|
:root {
|
|
--bg: #f6f8f7;
|
|
--panel: #ffffff;
|
|
--ink: #1f2a2e;
|
|
--muted: #617076;
|
|
--line: #d9e2df;
|
|
--accent: #0f6d62;
|
|
--accent-2: #2f7ca8;
|
|
--danger: #9f2f2f;
|
|
--ok: #156c3d;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
body {
|
|
margin: 0;
|
|
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
color: var(--ink);
|
|
background: linear-gradient(180deg, #ffffff 0%, var(--bg) 100%);
|
|
}
|
|
.shell {
|
|
max-width: 1180px;
|
|
margin: 0 auto;
|
|
padding: 1.25rem 1rem 2rem;
|
|
}
|
|
.hero {
|
|
display: grid;
|
|
gap: 0.8rem;
|
|
grid-template-columns: 1.2fr 0.8fr;
|
|
align-items: start;
|
|
margin-bottom: 1rem;
|
|
}
|
|
.hero-panel, .panel {
|
|
background: color-mix(in srgb, var(--panel) 94%, white 6%);
|
|
border: 1px solid var(--line);
|
|
border-radius: 12px;
|
|
padding: 1rem;
|
|
box-shadow: 0 16px 40px rgba(31, 42, 46, 0.06);
|
|
}
|
|
h1, h2, h3 { margin: 0 0 0.75rem; }
|
|
h1 { font-size: clamp(1.45rem, 2.3vw, 2.35rem); line-height: 1.08; }
|
|
h2 { font-size: 1.2rem; }
|
|
h3 { font-size: 1rem; }
|
|
p { margin: 0; color: var(--muted); line-height: 1.5; }
|
|
.actions, .toolbar {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 0.55rem;
|
|
margin-top: 0.8rem;
|
|
}
|
|
button {
|
|
border: 0;
|
|
border-radius: 999px;
|
|
padding: 0.65rem 0.95rem;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
color: white;
|
|
background: var(--ink);
|
|
}
|
|
button.secondary { background: var(--accent); }
|
|
button.ghost {
|
|
background: transparent;
|
|
color: var(--ink);
|
|
border: 1px solid var(--line);
|
|
}
|
|
button.danger { background: var(--danger); }
|
|
.grid {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
}
|
|
label {
|
|
display: block;
|
|
font-size: 0.84rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
input, textarea {
|
|
width: 100%;
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 0.58rem 0.7rem;
|
|
font: inherit;
|
|
background: #fff;
|
|
}
|
|
textarea { min-height: 7rem; resize: vertical; }
|
|
.users {
|
|
display: grid;
|
|
gap: 0.75rem;
|
|
margin-top: 0.75rem;
|
|
}
|
|
.user-card {
|
|
border: 1px solid var(--line);
|
|
border-radius: 10px;
|
|
padding: 0.8rem;
|
|
background: #fbfdfc;
|
|
}
|
|
.user-head {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-bottom: 0.55rem;
|
|
}
|
|
.user-card textarea {
|
|
min-height: 4.5rem;
|
|
}
|
|
.user-grid {
|
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
}
|
|
.json-field {
|
|
grid-column: span 2;
|
|
}
|
|
.defaults-panel {
|
|
margin-bottom: 1rem;
|
|
}
|
|
.defaults-panel summary {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 1rem;
|
|
cursor: pointer;
|
|
list-style: none;
|
|
}
|
|
.defaults-panel summary::-webkit-details-marker {
|
|
display: none;
|
|
}
|
|
.defaults-panel summary h2 {
|
|
margin: 0;
|
|
}
|
|
.summary-action {
|
|
color: var(--accent);
|
|
font-size: 0.9rem;
|
|
}
|
|
.defaults-panel[open] .defaults-grid {
|
|
margin-top: 0.85rem;
|
|
}
|
|
.status {
|
|
margin-top: 0.8rem;
|
|
padding: 0.65rem 0.8rem;
|
|
border-radius: 8px;
|
|
background: #edf2f0;
|
|
color: var(--ink);
|
|
}
|
|
.status.ok { background: rgba(21, 108, 61, 0.12); color: var(--ok); }
|
|
.status.error { background: rgba(159, 47, 47, 0.12); color: var(--danger); }
|
|
.steps {
|
|
display: grid;
|
|
gap: 0.5rem;
|
|
margin-top: 0.75rem;
|
|
}
|
|
.step {
|
|
padding: 0.65rem 0.8rem;
|
|
border-radius: 8px;
|
|
background: #eef4f2;
|
|
}
|
|
code {
|
|
font-family: "SFMono-Regular", Consolas, monospace;
|
|
background: rgba(15, 109, 98, 0.08);
|
|
padding: 0.15rem 0.35rem;
|
|
border-radius: 6px;
|
|
}
|
|
@media (max-width: 860px) {
|
|
.hero, .grid, .user-grid { grid-template-columns: 1fr; }
|
|
.json-field { grid-column: span 1; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main class="shell">
|
|
<section class="hero">
|
|
<div class="hero-panel">
|
|
<h1>Configura Fiscality senza modificare il JSON a mano.</h1>
|
|
<p>Crea gli utenti aziendali, controlla porte e nomi dei container, salva in <code>manager/config.json</code>, poi esegui <code>./install.sh</code>.</p>
|
|
<div class="actions">
|
|
<button id="saveButton" class="secondary">Salva configurazione</button>
|
|
<button id="validateButton">Valida</button>
|
|
<button id="downloadButton" class="ghost">Scarica JSON</button>
|
|
<button id="addUserButton" class="ghost">Aggiungi utente</button>
|
|
</div>
|
|
<div id="status" class="status">Caricamento configurazione...</div>
|
|
</div>
|
|
<div class="panel">
|
|
<h2>Procedura consigliata</h2>
|
|
<div class="steps">
|
|
<div class="step">1. Apri questa pagina con <code>./configure.sh</code>.</div>
|
|
<div class="step">2. Aggiungi una voce per ogni dipendente o postazione che deve usare Fiscality.</div>
|
|
<div class="step">3. Salva la configurazione, poi installa con <code>./install.sh</code>.</div>
|
|
<div class="step">4. Comunica agli utenti la porta dedicata o l'URL gateway condiviso, se lo abiliti.</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<details class="panel defaults-panel">
|
|
<summary>
|
|
<h2>Valori predefiniti</h2>
|
|
<span class="summary-action">Mostra/nascondi</span>
|
|
</summary>
|
|
<div class="grid defaults-grid">
|
|
<div>
|
|
<label for="containerPrefix">Prefisso container</label>
|
|
<input id="containerPrefix" type="text">
|
|
</div>
|
|
<div>
|
|
<label for="memLimit">Limite memoria</label>
|
|
<input id="memLimit" type="text" placeholder="3g">
|
|
</div>
|
|
<div>
|
|
<label for="memswapLimit">Limite swap</label>
|
|
<input id="memswapLimit" type="text" placeholder="5g">
|
|
</div>
|
|
<div>
|
|
<label for="privileged">Privilegiato</label>
|
|
<input id="privileged" type="text" placeholder="true o false">
|
|
</div>
|
|
<div>
|
|
<label for="defaultEnv">Ambiente predefinito JSON</label>
|
|
<textarea id="defaultEnv"></textarea>
|
|
</div>
|
|
<div>
|
|
<label for="defaultVolumes">Volumi predefiniti JSON</label>
|
|
<textarea id="defaultVolumes"></textarea>
|
|
</div>
|
|
<div>
|
|
<label for="defaultAdvanced">Altre impostazioni predefinite JSON</label>
|
|
<textarea id="defaultAdvanced"></textarea>
|
|
</div>
|
|
</div>
|
|
</details>
|
|
|
|
<section class="panel">
|
|
<div class="toolbar">
|
|
<h2 style="margin-right:auto">Utenti</h2>
|
|
</div>
|
|
<div id="users" class="users"></div>
|
|
</section>
|
|
</main>
|
|
|
|
<template id="userTemplate">
|
|
<article class="user-card">
|
|
<div class="user-head">
|
|
<h3>Utente</h3>
|
|
<button class="danger remove-user" type="button">Rimuovi</button>
|
|
</div>
|
|
<div class="grid user-grid">
|
|
<div>
|
|
<label>ID utente</label>
|
|
<input data-field="id" type="text" placeholder="mario">
|
|
</div>
|
|
<div>
|
|
<label>Porta</label>
|
|
<input data-field="port" type="number" placeholder="3001">
|
|
</div>
|
|
<div>
|
|
<label>Password</label>
|
|
<input data-field="password" type="password" placeholder="password" autocomplete="new-password">
|
|
</div>
|
|
<div>
|
|
<label>Nome container personalizzato</label>
|
|
<input data-field="container_name" type="text" placeholder="opzionale">
|
|
</div>
|
|
<div class="json-field">
|
|
<label>Ambiente aggiuntivo JSON</label>
|
|
<textarea data-field="environment"></textarea>
|
|
</div>
|
|
<div class="json-field">
|
|
<label>Volumi JSON</label>
|
|
<textarea data-field="volumes"></textarea>
|
|
</div>
|
|
<div class="json-field">
|
|
<label>Altre impostazioni utente JSON</label>
|
|
<textarea data-field="advanced"></textarea>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
</template>
|
|
|
|
<script>
|
|
const statusEl = document.getElementById("status");
|
|
const usersEl = document.getElementById("users");
|
|
const template = document.getElementById("userTemplate");
|
|
|
|
const defaultConfig = {
|
|
defaults: {
|
|
container_prefix: "fis-",
|
|
environment: { PUID: "1000", PGID: "1000" },
|
|
volumes: { "v-conf-{id}": "/config" },
|
|
mem_limit: "3g",
|
|
memswap_limit: "5g",
|
|
privileged: true
|
|
},
|
|
users: []
|
|
};
|
|
|
|
async function readJsonResponse(response) {
|
|
const contentType = response.headers.get("content-type") || "";
|
|
const text = await response.text();
|
|
|
|
if (!contentType.includes("application/json")) {
|
|
throw new Error(
|
|
"Backend del configuratore non disponibile. Apri questa pagina tramite ./configure.sh all'indirizzo http://127.0.0.1:8765, non da un'anteprima dell'IDE."
|
|
);
|
|
}
|
|
|
|
try {
|
|
return JSON.parse(text);
|
|
} catch (error) {
|
|
throw new Error("Il backend ha restituito JSON non valido.");
|
|
}
|
|
}
|
|
|
|
function setStatus(message, kind = "") {
|
|
statusEl.textContent = translateMessage(message);
|
|
statusEl.className = `status ${kind}`.trim();
|
|
}
|
|
|
|
function translateMessage(message) {
|
|
const replacements = [
|
|
[/^Duplicate user id: (.+)$/, "ID utente duplicato: $1"],
|
|
[/^Duplicate port: (.+)$/, "Porta duplicata: $1"],
|
|
[/^Duplicate container_name: (.+)$/, "Nome container duplicato: $1"],
|
|
[/^User (.+) must define at least one volume$/, "L'utente $1 deve definire almeno un volume"],
|
|
[/^User (.+) has duplicate volume mount targets$/, "L'utente $1 ha destinazioni di montaggio volume duplicate"],
|
|
[/^User (.+) must define environment\.PASSWORD$/, "L'utente $1 deve definire environment.PASSWORD"],
|
|
[/^Configuration must contain at least one user$/, "La configurazione deve contenere almeno un utente"]
|
|
];
|
|
|
|
for (const [pattern, replacement] of replacements) {
|
|
if (pattern.test(message)) {
|
|
return message.replace(pattern, replacement);
|
|
}
|
|
}
|
|
|
|
return message;
|
|
}
|
|
|
|
function managerRestartMessage(result, savedMessage) {
|
|
const restart = result.manager_restart;
|
|
if (!restart?.attempted) {
|
|
return {
|
|
message: `${savedMessage} Nessuna modifica rilevata: restart non necessario.`,
|
|
kind: "ok"
|
|
};
|
|
}
|
|
if (restart.ok) {
|
|
return {
|
|
message: `${savedMessage} Manager fis_manager riavviato.`,
|
|
kind: "ok"
|
|
};
|
|
}
|
|
return {
|
|
message: `${savedMessage} Attenzione: restart di fis_manager non riuscito: ${restart.error || "errore sconosciuto"}`,
|
|
kind: "error"
|
|
};
|
|
}
|
|
|
|
function stringify(value) {
|
|
return JSON.stringify(value ?? {}, null, 2);
|
|
}
|
|
|
|
function parseJsonField(value, label) {
|
|
if (!value.trim()) {
|
|
return {};
|
|
}
|
|
try {
|
|
return JSON.parse(value);
|
|
} catch (error) {
|
|
throw new Error(`${label} deve contenere JSON valido`);
|
|
}
|
|
}
|
|
|
|
function renderDefaults(config) {
|
|
const advanced = { ...(config.defaults ?? {}) };
|
|
delete advanced.container_prefix;
|
|
delete advanced.mem_limit;
|
|
delete advanced.memswap_limit;
|
|
delete advanced.privileged;
|
|
delete advanced.environment;
|
|
delete advanced.volumes;
|
|
|
|
document.getElementById("containerPrefix").value = config.defaults.container_prefix ?? "fis-";
|
|
document.getElementById("memLimit").value = config.defaults.mem_limit ?? "";
|
|
document.getElementById("memswapLimit").value = config.defaults.memswap_limit ?? "";
|
|
document.getElementById("privileged").value = String(config.defaults.privileged ?? false);
|
|
document.getElementById("defaultEnv").value = stringify(config.defaults.environment);
|
|
document.getElementById("defaultVolumes").value = stringify(config.defaults.volumes);
|
|
document.getElementById("defaultAdvanced").value = stringify(advanced);
|
|
}
|
|
|
|
function addUserCard(user = {}) {
|
|
const node = template.content.firstElementChild.cloneNode(true);
|
|
const advanced = { ...user };
|
|
delete advanced.id;
|
|
delete advanced.port;
|
|
delete advanced.container_name;
|
|
delete advanced.environment;
|
|
delete advanced.volumes;
|
|
|
|
node.querySelector('[data-field="id"]').value = user.id ?? "";
|
|
node.querySelector('[data-field="port"]').value = user.port ?? "";
|
|
node.querySelector('[data-field="password"]').value = user.environment?.PASSWORD ?? "";
|
|
node.querySelector('[data-field="container_name"]').value = user.container_name ?? "";
|
|
|
|
const environment = { ...(user.environment ?? {}) };
|
|
delete environment.PASSWORD;
|
|
node.querySelector('[data-field="environment"]').value = stringify(environment);
|
|
node.querySelector('[data-field="volumes"]').value = stringify(user.volumes ?? {});
|
|
node.querySelector('[data-field="advanced"]').value = stringify(advanced);
|
|
|
|
node.querySelector(".remove-user").addEventListener("click", () => {
|
|
node.remove();
|
|
});
|
|
|
|
usersEl.appendChild(node);
|
|
}
|
|
|
|
function renderUsers(config) {
|
|
usersEl.innerHTML = "";
|
|
config.users.forEach(addUserCard);
|
|
if (config.users.length === 0) {
|
|
addUserCard({ port: 3001 });
|
|
}
|
|
}
|
|
|
|
function collectConfig() {
|
|
const defaultAdvanced = parseJsonField(document.getElementById("defaultAdvanced").value, "Altre impostazioni predefinite");
|
|
const config = {
|
|
defaults: {
|
|
...defaultAdvanced,
|
|
container_prefix: document.getElementById("containerPrefix").value.trim() || "fis-",
|
|
mem_limit: document.getElementById("memLimit").value.trim() || null,
|
|
memswap_limit: document.getElementById("memswapLimit").value.trim() || null,
|
|
privileged: document.getElementById("privileged").value.trim().toLowerCase() === "true",
|
|
environment: parseJsonField(document.getElementById("defaultEnv").value, "Ambiente predefinito"),
|
|
volumes: parseJsonField(document.getElementById("defaultVolumes").value, "Volumi predefiniti")
|
|
},
|
|
users: []
|
|
};
|
|
|
|
usersEl.querySelectorAll(".user-card").forEach((card) => {
|
|
const environment = parseJsonField(card.querySelector('[data-field="environment"]').value, "Ambiente utente");
|
|
const password = card.querySelector('[data-field="password"]').value.trim();
|
|
if (password) {
|
|
environment.PASSWORD = password;
|
|
}
|
|
|
|
const advanced = parseJsonField(card.querySelector('[data-field="advanced"]').value, "Altre impostazioni utente");
|
|
config.users.push({
|
|
...advanced,
|
|
id: card.querySelector('[data-field="id"]').value.trim(),
|
|
port: Number(card.querySelector('[data-field="port"]').value),
|
|
container_name: card.querySelector('[data-field="container_name"]').value.trim() || undefined,
|
|
environment,
|
|
volumes: parseJsonField(card.querySelector('[data-field="volumes"]').value, "Volumi utente")
|
|
});
|
|
});
|
|
|
|
return config;
|
|
}
|
|
|
|
async function loadConfig() {
|
|
try {
|
|
const response = await fetch("/api/config");
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
"Backend del configuratore non disponibile. Avvialo con ./configure.sh e apri http://127.0.0.1:8765."
|
|
);
|
|
}
|
|
const config = await readJsonResponse(response);
|
|
renderDefaults(config);
|
|
renderUsers(config);
|
|
setStatus("Configurazione caricata. Controlla i valori e salva quando pronta.");
|
|
} catch (error) {
|
|
renderDefaults(defaultConfig);
|
|
renderUsers(defaultConfig);
|
|
setStatus(error.message, "error");
|
|
}
|
|
}
|
|
|
|
async function saveConfig() {
|
|
try {
|
|
const payload = collectConfig();
|
|
const response = await fetch("/api/config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const result = await readJsonResponse(response);
|
|
if (!response.ok || !result.ok) {
|
|
throw new Error(result.error || "Impossibile salvare la configurazione");
|
|
}
|
|
renderDefaults(result.config);
|
|
renderUsers(result.config);
|
|
const status = managerRestartMessage(result, "Configurazione salvata in manager/config.json.");
|
|
setStatus(status.message, status.kind);
|
|
} catch (error) {
|
|
setStatus(error.message, "error");
|
|
}
|
|
}
|
|
|
|
async function validateConfig() {
|
|
try {
|
|
const payload = collectConfig();
|
|
const response = await fetch("/api/config", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
const result = await readJsonResponse(response);
|
|
if (!response.ok || !result.ok) {
|
|
throw new Error(result.error || "Validazione non riuscita");
|
|
}
|
|
renderDefaults(result.config);
|
|
renderUsers(result.config);
|
|
const status = managerRestartMessage(result, "Validazione completata e configurazione normalizzata salvata.");
|
|
setStatus(status.message, status.kind);
|
|
} catch (error) {
|
|
setStatus(error.message, "error");
|
|
}
|
|
}
|
|
|
|
function downloadConfig() {
|
|
try {
|
|
const payload = collectConfig();
|
|
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement("a");
|
|
link.href = url;
|
|
link.download = "config.json";
|
|
link.click();
|
|
URL.revokeObjectURL(url);
|
|
setStatus("config.json scaricato. Usalo solo se ti serve una copia esterna.", "ok");
|
|
} catch (error) {
|
|
setStatus(error.message, "error");
|
|
}
|
|
}
|
|
|
|
document.getElementById("saveButton").addEventListener("click", saveConfig);
|
|
document.getElementById("validateButton").addEventListener("click", validateConfig);
|
|
document.getElementById("downloadButton").addEventListener("click", downloadConfig);
|
|
document.getElementById("addUserButton").addEventListener("click", () => addUserCard({ port: 3001 }));
|
|
|
|
loadConfig();
|
|
</script>
|
|
</body>
|
|
</html>
|