feat: version 2.15.0 -- instalador del agente GLPI, filtro de componentes y token recuperable

Instalador .bat generado en el navegador, casilla para ocultar actualizaciones/drivers/componentes
en Software, buscador por pestaña en el detalle, GUIDs como componentes y tokens de ingesta
cifrados (migraciones 024 y 025).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Milton Gonzalez
2026-10-20 08:17:53 -05:00
parent e25c2c3b35
commit d5f09dc91b
15 changed files with 151 additions and 12 deletions
+30 -2
View File
@@ -1,11 +1,11 @@
import { useState } from 'react';
import { Pencil, Save, Plus, Trash2, Power, Copy, Info } from 'lucide-react';
import { Pencil, Save, Plus, Trash2, Power, Copy, Info, Download } from 'lucide-react';
import { useUi } from '../../context/ui-context.jsx';
import { useAuth } from '../../context/auth-context.jsx';
import { useAsync, useAction } from '../../hooks/useAsync.js';
import {
listDevices, getDevice, updateDevice, getDevicesDashboard, listSoftware, listSoftwareDevices,
listInventoryTokens, createInventoryToken, setInventoryTokenActive, deleteInventoryToken,
listInventoryTokens, createInventoryToken, setInventoryTokenActive, deleteInventoryToken, getInventoryTokenSecret,
} from '../../services/devices.js';
import { listUsers } from '../../services/users.js';
import { listDepartments } from '../../services/departments.js';
@@ -18,6 +18,7 @@ import { ListView } from '../../components/ListView.jsx';
import { Tabs } from '../../components/Tabs.jsx';
import { Badge } from '../../components/Badge.jsx';
import { KpiCard } from '../../components/KpiCard.jsx';
import { buildInstallerBat, downloadTextFile, GLPI_AGENT_VERSION } from './agentInstaller.js';
const KINDS = ['desktop', 'laptop', 'server', 'virtual', 'printer', 'network', 'other'];
const fmtDate = (v) => (v ? String(v).slice(0, 16).replace('T', ' ') : '—');
@@ -381,12 +382,26 @@ function AgentsTab() {
const [label, setLabel] = useState('');
const [created, setCreated] = useState(null); // { label, token } mostrado UNA vez
const serverUrl = `${window.location.origin}/api/inventory`;
const [installToken, setInstallToken] = useState('');
const [installTag, setInstallTag] = useState('');
function downloadFromToken(tk) {
runAction(async () => {
const { token } = await getInventoryTokenSecret(tk.id);
downloadTextFile('instalar-glpi-agent.bat', buildInstallerBat({ serverUrl, token, tag: installTag }));
});
}
function downloadInstaller() {
downloadTextFile('instalar-glpi-agent.bat', buildInstallerBat({ serverUrl, token: installToken.trim(), tag: installTag }));
}
function create() {
runAction(async () => {
const res = await createInventoryToken(label.trim());
setLabel('');
setCreated(res);
setInstallToken(res.token);
await reload();
});
}
@@ -418,6 +433,17 @@ function AgentsTab() {
<div style={{ fontSize: '.75rem', marginTop: 8 }} className="text-muted">{t('eq_agent_setup_auth')}</div>
</div>
<div className="kpi-card" style={{ marginBottom: 16 }}>
<div className="kpi-label" style={{ marginBottom: 6 }}>{t('eq_installer_title')} (v{GLPI_AGENT_VERSION})</div>
<div style={{ fontSize: '.8rem', marginBottom: 8 }}>{t('eq_installer_hint')} {t('eq_installer_row_hint')}</div>
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap', alignItems: 'center' }}>
<input className="input" style={{ width: 320 }} placeholder={t('eq_installer_token')} value={installToken} onChange={(e) => setInstallToken(e.target.value)} />
<input className="input" style={{ width: 180 }} placeholder={t('eq_installer_tag')} value={installTag} onChange={(e) => setInstallTag(e.target.value)} />
<Button variant="accent" icon={Download} onClick={downloadInstaller} disabled={!installToken.trim()}>{t('eq_installer_download')}</Button>
</div>
<div style={{ fontSize: '.75rem', marginTop: 8 }} className="text-muted">{t('eq_installer_note')}</div>
</div>
<div className="toolbar">
<div className="toolbar-left">
<input className="input" style={{ width: 260 }} placeholder={t('eq_token_label')} value={label} onChange={(e) => setLabel(e.target.value)} />
@@ -447,6 +473,8 @@ function AgentsTab() {
key: 'actions', label: '', sortable: false,
render: (r) => (
<span style={{ display: 'flex', gap: 4 }} onClick={(e) => e.stopPropagation()}>
<button className="icon-btn" title={r.has_secret ? t('eq_installer_download') : t('eq_token_no_secret')} disabled={!r.has_secret}
onClick={() => downloadFromToken(r)}><Download size={15} /></button>
<button className="icon-btn" title={r.is_active ? t('eq_token_disable') : t('eq_token_enable')}
onClick={() => runAction(async () => { await setInventoryTokenActive(r.id, !r.is_active); await reload(); })}><Power size={15} /></button>
<button className="icon-btn" title={t('common_delete')} onClick={() => confirmDelete(r)}><Trash2 size={15} /></button>
@@ -0,0 +1,56 @@
// Genera en el navegador el instalador (.bat) del agente GLPI con el servidor y el token ya
// puestos. No pasa por el backend: el token en claro solo existe en la pantalla (en BD queda su hash).
export const GLPI_AGENT_VERSION = '1.21';
export function buildInstallerBat({ serverUrl, token, tag }) {
const ver = GLPI_AGENT_VERSION;
const msiName = `GLPI-Agent-${ver}-x64.msi`;
const safeTag = String(tag || '').replace(/[^\w .-]/g, '').trim();
const lines = [
'@echo off',
'setlocal',
`title Instalador GLPI Agent ${ver} - Project`,
`rem Generado por Project (${serverUrl}). Si el .msi esta junto a este archivo se usa; si no, se descarga.`,
'net session >nul 2>&1',
'if %errorlevel% neq 0 (',
' echo Solicitando permisos de administrador...',
` powershell -NoProfile -Command "Start-Process -FilePath '%~f0' -Verb RunAs"`,
' exit /b',
')',
`set "MSI=%~dp0${msiName}"`,
'if exist "%MSI%" goto instalar',
`set "MSI=%TEMP%\\${msiName}"`,
'if exist "%MSI%" goto instalar',
`echo Descargando GLPI Agent ${ver}...`,
`powershell -NoProfile -ExecutionPolicy Bypass -Command "[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; Invoke-WebRequest -UseBasicParsing 'https://github.com/glpi-project/glpi-agent/releases/download/${ver}/${msiName}' -OutFile '%MSI%'"`,
'if errorlevel 1 (',
` echo ERROR: no se pudo descargar. Copie ${msiName} junto a este archivo y ejecute de nuevo.`,
' pause',
' exit /b 1',
')',
':instalar',
'echo Instalando y configurando el agente...',
`start /wait msiexec /i "%MSI%" /qn /norestart SERVER="${serverUrl}" USER="project" PASSWORD="${token}"${safeTag ? ` TAG="${safeTag}"` : ''} RUNNOW=1`,
'set RC=%errorlevel%',
'if %RC% neq 0 if %RC% neq 3010 (',
' echo ERROR: la instalacion fallo ^(codigo %RC%^).',
' pause',
' exit /b %RC%',
')',
'echo.',
'sc query glpi-agent | find "RUNNING" >nul && (echo Listo: el agente esta instalado y en ejecucion. El equipo aparecera en Project en unos minutos.) || (echo Instalado, pero el servicio no esta en ejecucion. Revise services.msc.)',
'pause',
];
return lines.join('\r\n') + '\r\n';
}
export function downloadTextFile(filename, content) {
const url = URL.createObjectURL(new Blob([content], { type: 'application/octet-stream' }));
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(url);
}