diff --git a/BASE_DE_DATOS.md b/BASE_DE_DATOS.md
index ea89b42..28951c3 100644
--- a/BASE_DE_DATOS.md
+++ b/BASE_DE_DATOS.md
@@ -44,6 +44,7 @@
| `vault_entries` | repositorio de credenciales privado por usuario (menú "Contraseñas"), contraseña/notas cifradas |
| `devices` | inventario de equipos (menú "Equipos"), upsert por `deviceid` del agente GLPI; baja lógica con `status` |
| `device_cpus` / `device_memories` / `device_disks` / `device_volumes` / `device_networks` / `device_monitors` / `device_software` | componentes de cada equipo, reemplazados completos en cada reporte |
+| `device_accounts` | cuentas que reporta el agente: sesión iniciada, usuarios locales y grupos locales (`kind`), reemplazadas en cada reporte (migración `027`) |
| `device_changes` | diff entre reportes (RAM, discos, software instalado/desinstalado) |
| `device_viewers` | usuarios con acceso de solo lectura compartido al menú Equipos (`user_id`, `granted_by`) |
| `inventory_tokens` | tokens de ingesta del agente (solo hash sha256) |
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f307635..0125632 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog - project.linkedcorp.net
+## [2.17.0] - 2026-10-20
+### Added
+- Equipos → detalle: pestaña **Usuarios** (después de General) con el usuario que tiene sesión
+ iniciada, el total y la lista de usuarios locales de Windows (con SID) y los grupos locales.
+ Solo cuentas locales del equipo (no dominio ni Microsoft 365). Migración `027`
+ (`device_accounts`); los equipos la llenan en su siguiente reporte.
+
## [2.16.5] - 2026-10-20
### Changed
- Equipos → listado: se quita el botón "Información" del final de cada fila; el detalle del equipo
diff --git a/SPEC.md b/SPEC.md
index 6056776..37b3433 100644
--- a/SPEC.md
+++ b/SPEC.md
@@ -81,7 +81,7 @@ tiene cada cosa — sin que cargar la información sea tedioso para quien lo usa
- Menú "Equipos" visible para `superadmin`, `admin` y `it_manager`. Inventario automático de
hardware y software alimentado por el **agente GLPI oficial** instalado en cada equipo (sin
servidor GLPI). Diseño en `docs/ARQUITECTURA-INVENTARIO.md`.
-- Pestañas: Equipos (KPIs + listado + detalle General/Hardware/Red/Software/Historial),
+- Pestañas: Equipos (KPIs + listado + detalle General/Usuarios/Hardware/Red/Software/Historial),
Software (transversal: en cuántos equipos está instalado) y Agentes (tokens de ingesta,
solo `superadmin`/`admin`) y Compartir (da acceso de solo lectura a otros usuarios, `superadmin`/`admin`).
- Edición manual solo de: estado (activo/de baja), usuario asignado, departamento y notas; el
diff --git a/VERSION b/VERSION
index ffa5fee..d76bd2b 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.16.5
+2.17.0
diff --git a/backend/database/migrations/027_device_accounts.sql b/backend/database/migrations/027_device_accounts.sql
new file mode 100644
index 0000000..b4cb461
--- /dev/null
+++ b/backend/database/migrations/027_device_accounts.sql
@@ -0,0 +1,12 @@
+-- Cuentas del equipo que reporta el agente: usuarios con sesion iniciada (USERS), usuarios locales
+-- (LOCAL_USERS) y grupos locales (LOCAL_GROUPS). Se reemplazan completas en cada reporte. Idempotente.
+CREATE TABLE IF NOT EXISTS device_accounts (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ device_id INT NOT NULL,
+ kind ENUM('session','local_user','local_group') NOT NULL,
+ name VARCHAR(190) NOT NULL,
+ domain VARCHAR(190) NULL,
+ sid VARCHAR(190) NULL,
+ KEY idx_device_accounts_device (device_id, kind),
+ CONSTRAINT fk_device_accounts_device FOREIGN KEY (device_id) REFERENCES devices(id) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/backend/package.json b/backend/package.json
index c895dee..b27a535 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "project-linkedcorp-backend",
- "version": "2.16.5",
+ "version": "2.17.0",
"private": true,
"type": "module",
"main": "src/server.js",
diff --git a/backend/src/modules/devices/normalizer.js b/backend/src/modules/devices/normalizer.js
index 4c448d9..024d1bf 100644
--- a/backend/src/modules/devices/normalizer.js
+++ b/backend/src/modules/devices/normalizer.js
@@ -7,7 +7,7 @@ import { XMLParser } from 'fast-xml-parser';
const XML_ARRAY_SECTIONS = new Set([
'cpus', 'memories', 'storages', 'drives', 'networks', 'monitors', 'softwares', 'printers',
- 'users', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus',
+ 'users', 'local_users', 'local_groups', 'videos', 'sounds', 'batteries', 'controllers', 'ports', 'slots', 'antivirus',
]);
const xmlParser = new XMLParser({ ignoreAttributes: true, parseTagValue: false, trimValues: true });
@@ -144,6 +144,14 @@ export function normalizeInventory(parsed) {
})
.filter((s) => s.name);
+ // Cuentas: sesion iniciada (USERS), usuarios locales y grupos locales (solo cuentas LOCALES del equipo).
+ const account = (kind, name, domain, sid) => ({ kind, name: str(name, 190), domain: str(domain, 190), sid: str(sid, 190) });
+ const accounts = [
+ ...asArray(c.users).map((x) => account('session', x.login, x.domain, null)),
+ ...asArray(c.local_users).map((x) => account('local_user', x.name, null, x.id)),
+ ...asArray(c.local_groups).map((x) => account('local_group', x.name, null, x.id)),
+ ].filter((a) => a.name);
+
// Resumenes para listado/KPIs.
const hostname = str(hw.name || nd.name || printer.name || os.fqdn, 190);
const upIfaces = networks.filter((n) => n.ip && !/^(127\.|169\.254\.|::1|fe80)/i.test(n.ip) && !n.ip.includes(':'));
@@ -190,5 +198,5 @@ export function normalizeInventory(parsed) {
if (c[key] && (!Array.isArray(c[key]) || c[key].length)) extra[key] = c[key];
}
- return { device, cpus, memories, disks, volumes, networks, monitors, software, extra };
+ return { device, cpus, memories, disks, volumes, networks, monitors, software, accounts, extra };
}
diff --git a/backend/src/modules/devices/repository.js b/backend/src/modules/devices/repository.js
index 422dec8..2bb0377 100644
--- a/backend/src/modules/devices/repository.js
+++ b/backend/src/modules/devices/repository.js
@@ -13,6 +13,7 @@ const CHILD_TABLES = {
volumes: { table: 'device_volumes', cols: ['mount', 'label', 'filesystem', 'type', 'total_mb', 'free_mb'] },
networks: { table: 'device_networks', cols: ['description', 'mac', 'ip', 'mask', 'gateway', 'type', 'status'] },
monitors: { table: 'device_monitors', cols: ['manufacturer', 'model', 'serial'] },
+ accounts: { table: 'device_accounts', cols: ['kind', 'name', 'domain', 'sid'] },
software: { table: 'device_software', cols: ['name', 'version', 'publisher', 'install_date', 'category', 'is_component'] },
};
diff --git a/frontend/package.json b/frontend/package.json
index bdc574f..a6a17f7 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "project-linkedcorp-frontend",
"private": true,
- "version": "2.16.5",
+ "version": "2.17.0",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/frontend/src/i18n/messages.js b/frontend/src/i18n/messages.js
index c986f3c..3ae969c 100644
--- a/frontend/src/i18n/messages.js
+++ b/frontend/src/i18n/messages.js
@@ -210,6 +210,13 @@ export const messages = {
eq_share_remove: 'Quitar acceso',
eq_share_remove_title: 'Quitar acceso a Equipos',
eq_share_remove_msg: 'dejará de ver el menú Equipos.',
+ eq_tab_users: 'Usuarios',
+ eq_users_session: 'Con sesión iniciada',
+ eq_users_local_count: 'Usuarios locales (total)',
+ eq_groups_local_count: 'Grupos locales (total)',
+ eq_users_local: 'Usuarios locales de Windows',
+ eq_groups_local: 'Grupos locales',
+ eq_users_hint: 'Solo cuentas locales del equipo: no incluye usuarios de dominio ni de Microsoft 365, e incluye cuentas de sistema o de programas. Los grupos no indican a qué usuario pertenecen.',
eq_tab_summary: 'Resumen',
eq_show_components: 'Mostrar actualizaciones, drivers y componentes del sistema',
eq_tab_devices: 'Equipos',
@@ -685,6 +692,13 @@ export const messages = {
eq_share_remove: 'Remove access',
eq_share_remove_title: 'Remove access to Devices',
eq_share_remove_msg: 'will no longer see the Devices menu.',
+ eq_tab_users: 'Users',
+ eq_users_session: 'Signed in',
+ eq_users_local_count: 'Local users (total)',
+ eq_groups_local_count: 'Local groups (total)',
+ eq_users_local: 'Windows local users',
+ eq_groups_local: 'Local groups',
+ eq_users_hint: 'Local accounts of the machine only: domain and Microsoft 365 users are not included, and system or application accounts are. Groups do not show which user belongs to them.',
eq_tab_summary: 'Summary',
eq_show_components: 'Show updates, drivers and system components',
eq_tab_devices: 'Devices',
diff --git a/frontend/src/pages/Equipos/EquiposPage.jsx b/frontend/src/pages/Equipos/EquiposPage.jsx
index 52dadca..0070a9f 100644
--- a/frontend/src/pages/Equipos/EquiposPage.jsx
+++ b/frontend/src/pages/Equipos/EquiposPage.jsx
@@ -103,6 +103,7 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) {
const tabs = [
{ key: 'general', label: t('eq_tab_general') },
+ { key: 'users', label: t('eq_tab_users'), count: device.accounts.filter((a) => a.kind === 'local_user').length },
{ key: 'hardware', label: t('eq_tab_hardware') },
{ key: 'network', label: t('eq_tab_network'), count: device.networks.length },
{ key: 'software', label: t('eq_tab_software'), count: device.software.filter((r) => showComponents || !r.is_component).length },
@@ -152,6 +153,16 @@ function DeviceDetail({ id, onClose, onChanged, canEdit }) {