Usar clave SSH global del monitor
This commit is contained in:
+115
-12
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user