Usar clave SSH global del monitor

This commit is contained in:
2026-06-04 17:52:24 +02:00
parent e29df62f7a
commit d7fe855ec0
5 changed files with 228 additions and 37 deletions
+115 -12
View File
@@ -154,6 +154,45 @@
gap: 10px;
}
.ssh-key-panel {
margin-top: 16px;
display: grid;
gap: 12px;
padding: 12px;
border: 1px solid var(--line);
border-radius: 16px;
background: var(--panel-2);
}
.ssh-key-fields {
display: grid;
grid-template-columns: repeat(2, minmax(220px, 1fr));
gap: 10px;
}
.public-key-box {
display: grid;
gap: 8px;
}
.public-key-box textarea {
width: 100%;
min-height: 78px;
resize: vertical;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--input);
color: var(--text);
padding: 10px;
font: 13px ui-monospace, "Cascadia Code", Consolas, monospace;
}
.public-key-actions {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.threshold-row {
display: grid;
grid-template-columns: 110px repeat(3, minmax(120px, 1fr));
@@ -198,9 +237,8 @@
.field-port { grid-column: span 1; }
.field-username { grid-column: span 2; }
.field-auth { grid-column: span 2; }
.password-credential, .key-credential { grid-column: span 4; }
.password-credential { grid-column: span 4; }
.field-delete { grid-column: span 2; }
.device[data-auth-method="password"] .key-credential { display: none; }
.device[data-auth-method="key"] .password-credential { display: none; }
.credential-note {
@@ -262,6 +300,17 @@
}
button:hover, a:hover { border-color: var(--info); }
.secondary-action {
background: var(--panel);
border-color: var(--info);
color: var(--info);
font-weight: 650;
}
.secondary-action:hover {
background: color-mix(in srgb, var(--info) 18%, var(--panel));
}
.danger {
background: var(--offline-bg);
border-color: var(--offline-line);
@@ -396,11 +445,12 @@
@media (max-width: 1180px) {
.device { grid-template-columns: repeat(6, minmax(0, 1fr)); align-items: start; }
.field-name, .field-role, .field-host, .password-credential, .key-credential { grid-column: span 3; }
.field-name, .field-role, .field-host, .password-credential { grid-column: span 3; }
.field-id, .field-model, .field-location, .field-username, .field-auth, .field-active, .field-delete { grid-column: span 2; }
.field-port { grid-column: span 1; }
.credential-note { grid-column: span 4; }
.top-grid { grid-template-columns: repeat(2, 1fr); }
.ssh-key-fields { grid-template-columns: 1fr; }
.threshold-row { grid-template-columns: 90px repeat(3, minmax(100px, 1fr)); }
}
@@ -410,7 +460,7 @@
.top-grid, .device { grid-template-columns: 1fr; }
.field-active, .field-name, .field-id, .field-role, .field-model, .field-location,
.field-host, .field-port, .field-username, .field-auth, .password-credential,
.key-credential, .credential-note, .field-delete { grid-column: span 1; }
.credential-note, .field-delete { grid-column: span 1; }
.threshold-row { grid-template-columns: 1fr; }
}
</style>
@@ -452,6 +502,26 @@
<input id="sshTimeoutSeconds" type="number" min="2" step="1">
</label>
</div>
<div class="ssh-key-panel">
<h2>Clave SSH del monitor</h2>
<div class="ssh-key-fields">
<label title="Ruta de la clave privada dentro del contenedor. Esta clave se queda solo en el monitor.">Clave privada global
<input id="sshPrivateKeyPath" value="/ssh/monitor_rpi_ed25519">
</label>
<label title="Ruta de la clave publica dentro del contenedor. Esta es la clave que se copia a las RPi remotas.">Clave publica global
<input id="sshPublicKeyPath" value="/ssh/monitor_rpi_ed25519.pub">
</label>
</div>
<div class="public-key-box">
<label title="Copia esta clave publica en ~/.ssh/authorized_keys del usuario SSH de la RPi remota.">Clave publica para instalar en las RPi
<textarea id="sshPublicKey" readonly placeholder="Cargando clave publica..."></textarea>
</label>
<div class="public-key-actions">
<button class="secondary-action" id="copyPublicKey" type="button">Copiar clave publica</button>
<a class="secondary-action" id="downloadPublicKey" href="/api/ssh-public-key/download">Descargar .pub</a>
</div>
</div>
</div>
<div class="threshold-groups">
<div class="threshold-row">
<div class="threshold-title">Temperatura</div>
@@ -588,9 +658,10 @@
const devicesEl = document.querySelector("#devices");
const statusEl = document.querySelector("#status");
const dashboardLink = document.querySelector("#dashboardLink");
const publicKeyEl = document.querySelector("#sshPublicKey");
function deviceTemplate(device, index) {
const authMethod = device.privateKeyPath ? "key" : "password";
const authMethod = device.authMethod || (device.privateKeyPath ? "key" : "password");
return `
<div class="device" data-index="${index}" data-auth-method="${authMethod}">
<label class="field-name" title="Nombre visible de la tarjeta en el dashboard.">Nombre
@@ -628,11 +699,8 @@
<label class="password-credential" title="Password SSH. Se guarda en config.json dentro del volumen persistente. Si eliges clave privada, se guardara vacio.">Password
<input data-field="password" type="password" value="${device.password || ""}">
</label>
<label class="key-credential" title="Ruta de la clave privada SSH dentro del contenedor. Ejemplo: /ssh/carabanes_monitor_ed25519. Si eliges password, se guardara vacia.">Clave privada
<input data-field="privateKeyPath" value="${device.privateKeyPath || ""}">
</label>
<div class="credential-note">
Metodo activo: <strong>${authMethod === "key" ? "Clave privada" : "Password"}</strong>
Metodo activo: <strong>${authMethod === "key" ? "Clave privada global del monitor" : "Password"}</strong>
</div>
<label class="field-active" title="Si esta activo, este dispositivo se consulta por SSH. Si esta inactivo, se muestra deshabilitado y no se escanea.">Activo
<input data-field="active" type="checkbox" ${device.active ? "checked" : ""}>
@@ -648,6 +716,8 @@
document.querySelector("#refreshIntervalSeconds").value = state.config.refreshIntervalSeconds || 30;
document.querySelector("#idleScanIntervalSeconds").value = state.config.idleScanIntervalSeconds || 300;
document.querySelector("#sshTimeoutSeconds").value = state.config.sshTimeoutSeconds || 8;
document.querySelector("#sshPrivateKeyPath").value = state.config.sshPrivateKeyPath || "/ssh/monitor_rpi_ed25519";
document.querySelector("#sshPublicKeyPath").value = state.config.sshPublicKeyPath || `${document.querySelector("#sshPrivateKeyPath").value}.pub`;
document.querySelector("#warningTemp").value = state.config.temperatureThresholdsC?.warning || 60;
document.querySelector("#hotTemp").value = state.config.temperatureThresholdsC?.hot || 70;
document.querySelector("#criticalTemp").value = state.config.temperatureThresholdsC?.critical || 80;
@@ -671,6 +741,8 @@
state.config.refreshIntervalSeconds = Number(document.querySelector("#refreshIntervalSeconds").value || 30);
state.config.idleScanIntervalSeconds = Number(document.querySelector("#idleScanIntervalSeconds").value || 300);
state.config.sshTimeoutSeconds = Number(document.querySelector("#sshTimeoutSeconds").value || 8);
state.config.sshPrivateKeyPath = document.querySelector("#sshPrivateKeyPath").value || "/ssh/monitor_rpi_ed25519";
state.config.sshPublicKeyPath = document.querySelector("#sshPublicKeyPath").value || `${state.config.sshPrivateKeyPath}.pub`;
state.config.temperatureThresholdsC = {
...(state.config.temperatureThresholdsC || {}),
warning: Number(document.querySelector("#warningTemp").value || 60),
@@ -709,8 +781,9 @@
else device[field] = input.value;
});
const authMethod = row.querySelector("[data-auth-method]")?.value || "password";
if (authMethod === "key") device.password = "";
else device.privateKeyPath = "";
device.authMethod = authMethod === "key" ? "key" : "password";
device.privateKeyPath = "";
if (device.authMethod === "key") device.password = "";
return device;
});
}
@@ -719,6 +792,18 @@
const res = await fetch("/api/config", { cache: "no-store" });
state.config = await res.json();
render();
loadPublicKey().catch((error) => {
publicKeyEl.value = error.message;
});
}
async function loadPublicKey() {
publicKeyEl.value = "Cargando clave publica...";
const res = await fetch("/api/ssh-public-key", { cache: "no-store" });
const payload = await res.json();
if (!res.ok) throw new Error(payload.error || `HTTP ${res.status}`);
publicKeyEl.value = payload.publicKey || "";
document.querySelector("#sshPublicKeyPath").value = payload.publicKeyPath || document.querySelector("#sshPublicKeyPath").value;
}
async function save() {
@@ -762,6 +847,7 @@
username: "pi",
password: "",
privateKeyPath: "",
authMethod: "password",
model: "RPi 4",
role: "",
location: "Rack",
@@ -797,7 +883,24 @@
const row = selector.closest(".device");
row.dataset.authMethod = selector.value;
const note = row.querySelector(".credential-note strong");
if (note) note.textContent = selector.value === "key" ? "Clave privada" : "Password";
if (note) note.textContent = selector.value === "key" ? "Clave privada global del monitor" : "Password";
});
document.querySelector("#copyPublicKey").addEventListener("click", async () => {
const button = document.querySelector("#copyPublicKey");
try {
await navigator.clipboard.writeText(publicKeyEl.value);
button.classList.add("action-ok");
button.textContent = "Copiada";
setTimeout(() => {
button.classList.remove("action-ok");
button.textContent = "Copiar clave publica";
}, 1200);
} catch {
publicKeyEl.select();
statusEl.className = "status save-error";
statusEl.textContent = "No se pudo copiar automaticamente. Selecciona la clave y copiala manualmente.";
}
});
load().catch((error) => {