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:
@@ -191,6 +191,14 @@ export const messages = {
|
||||
nav_notes: 'Notas',
|
||||
nav_passwords: 'Contraseñas',
|
||||
nav_devices: 'Equipos',
|
||||
eq_installer_title: 'Instalador para Windows',
|
||||
eq_installer_row_hint: 'También puedes descargarlo con el ícono de cada token de la lista.',
|
||||
eq_token_no_secret: 'Token anterior: no se puede recuperar, genera uno nuevo',
|
||||
eq_installer_hint: 'Descarga un instalador (.bat) con el servidor y el token ya configurados: se ejecuta con doble clic en el equipo, sin configurar nada más.',
|
||||
eq_installer_token: 'Token (se llena solo al generar uno)',
|
||||
eq_installer_tag: 'Etiqueta opcional (ej. Contabilidad)',
|
||||
eq_installer_download: 'Descargar instalador',
|
||||
eq_installer_note: 'El .bat contiene el token: compártelo solo con quien instale. Descarga el agente oficial de GitHub; si el equipo no tiene internet, copia el .msi junto al .bat.',
|
||||
eq_tab_summary: 'Resumen',
|
||||
eq_show_components: 'Mostrar actualizaciones, drivers y componentes del sistema',
|
||||
eq_btn_info: 'Información',
|
||||
@@ -647,6 +655,14 @@ export const messages = {
|
||||
nav_notes: 'Notes',
|
||||
nav_passwords: 'Passwords',
|
||||
nav_devices: 'Devices',
|
||||
eq_installer_title: 'Windows installer',
|
||||
eq_installer_row_hint: 'You can also download it with the icon on each token in the list.',
|
||||
eq_token_no_secret: 'Older token: cannot be recovered, generate a new one',
|
||||
eq_installer_hint: 'Download an installer (.bat) with the server and token already set: double-click it on the machine, nothing else to configure.',
|
||||
eq_installer_token: 'Token (filled in when you generate one)',
|
||||
eq_installer_tag: 'Optional tag (e.g. Accounting)',
|
||||
eq_installer_download: 'Download installer',
|
||||
eq_installer_note: 'The .bat contains the token: share it only with whoever installs. It downloads the official agent from GitHub; if the machine has no internet, put the .msi next to the .bat.',
|
||||
eq_tab_summary: 'Summary',
|
||||
eq_show_components: 'Show updates, drivers and system components',
|
||||
eq_btn_info: 'Information',
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -14,3 +14,4 @@ export const listInventoryTokens = () => api.get('/devices/tokens');
|
||||
export const createInventoryToken = (label) => api.post('/devices/tokens', { label });
|
||||
export const setInventoryTokenActive = (id, isActive) => api.patch(`/devices/tokens/${id}`, { isActive });
|
||||
export const deleteInventoryToken = (id) => api.delete(`/devices/tokens/${id}`);
|
||||
export const getInventoryTokenSecret = (id) => api.get(`/devices/tokens/${id}/secret`);
|
||||
|
||||
Reference in New Issue
Block a user