feat: añadir agente de Windows y soporte para control remoto de ordenadores

This commit is contained in:
Naiel
2026-03-02 12:39:13 +00:00
parent 9d808ed63e
commit cb12894455
8 changed files with 820 additions and 2 deletions

View File

@@ -0,0 +1,46 @@
name: Build Windows Agent (Release)
on:
release:
types: [published]
workflow_dispatch:
permissions:
contents: write
jobs:
build-windows-agent:
runs-on: windows-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dependencies
shell: bash
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pyinstaller
- name: Build hidden EXE with PyInstaller
shell: bash
run: |
pyinstaller --noconfirm --clean --onefile --noconsole --name telesec-windows-agent python_sdk/windows_agent.py
- name: Upload workflow artifact
uses: actions/upload-artifact@v4
with:
name: telesec-windows-agent
path: dist/telesec-windows-agent.exe
- name: Upload asset to GitHub Release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v2
with:
files: dist/telesec-windows-agent.exe

9
.gitignore vendored
View File

@@ -3,3 +3,12 @@ radata/*
node_modules/*
.DS_Store
._*
# Python
__pycache__/*
*.pyc
*.pyo
*.pyd
*.egg-info/*
*.egg
.venv/*
venv/*

View File

@@ -1,2 +1,77 @@
# TeleSec
Nuevo programa de datos
## Python SDK (CouchDB directo)
Se añadió un SDK Python en `python_sdk/` para acceder directamente a CouchDB (sin replicación local), compatible con el formato de cifrado de `TS_encrypt`:
- Formato: `RSA{...}`
- Algoritmo: `CryptoJS.AES.encrypt(payload, secret)` (modo passphrase/OpenSSL)
### Instalación
```bash
pip install -r requirements.txt
```
### Uso rápido
```python
from python_sdk import TeleSecCouchDB
db = TeleSecCouchDB(
server_url="https://tu-couchdb",
dbname="telesec",
username="usuario",
password="clave",
secret="SECRET123",
)
# Guardar cifrado (como TS_encrypt)
db.put("personas", "abc123", {"nombre": "Ana"}, encrypt=True)
# Leer y descifrar
obj = db.get("personas", "abc123", decrypt=True)
# Listar una tabla
rows = db.list("personas", decrypt=True)
for row in rows:
print(row.id, row.data)
```
API principal:
- `TeleSecCouchDB.put(table, item_id, data, encrypt=True)`
- `TeleSecCouchDB.get(table, item_id, decrypt=True)`
- `TeleSecCouchDB.list(table, decrypt=True)`
- `TeleSecCouchDB.delete(table, item_id)`
- `ts_encrypt(value, secret)` / `ts_decrypt(value, secret)`
## Agente Windows (Gest-Aula > Ordenadores)
Se añadió soporte para control de ordenadores del aula:
- Tabla: `aulas_ordenadores`
- Campos reportados por agente: `Hostname`, `UsuarioActual`, `AppActualEjecutable`, `AppActualTitulo`, `LastSeenAt`
- Control remoto: `ShutdownBeforeDate` (programado desde web a `hora_servidor + 2 minutos`)
### Ejecutar agente en Windows
```bash
python -m python_sdk.windows_agent \
--server "https://tu-couchdb" \
--db "telesec" \
--user "usuario" \
--password "clave" \
--secret "SECRET123"
```
Opciones útiles:
- `--once`: una sola iteración
- `--interval 15`: intervalo (segundos)
- `--dry-run`: no apaga realmente, solo simula
### Hora de servidor (sin depender del reloj local)
El frontend y el agente usan la hora del servidor (cabecera HTTP `Date` de CouchDB) para comparar `ShutdownBeforeDate`.

3
python_sdk/__init__.py Normal file
View File

@@ -0,0 +1,3 @@
from .telesec_couchdb import TeleSecCouchDB, ts_encrypt, ts_decrypt
__all__ = ["TeleSecCouchDB", "ts_encrypt", "ts_decrypt"]

View File

@@ -0,0 +1,293 @@
import base64
import email.utils
import hashlib
import json
import os
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional
from urllib.parse import quote
import requests
from Crypto.Cipher import AES
class TeleSecCryptoError(Exception):
pass
class TeleSecCouchDBError(Exception):
pass
def _pkcs7_pad(data: bytes, block_size: int = 16) -> bytes:
pad_len = block_size - (len(data) % block_size)
return data + bytes([pad_len]) * pad_len
def _pkcs7_unpad(data: bytes, block_size: int = 16) -> bytes:
if not data or len(data) % block_size != 0:
raise TeleSecCryptoError("Invalid padded data length")
pad_len = data[-1]
if pad_len < 1 or pad_len > block_size:
raise TeleSecCryptoError("Invalid PKCS7 padding")
if data[-pad_len:] != bytes([pad_len]) * pad_len:
raise TeleSecCryptoError("Invalid PKCS7 padding bytes")
return data[:-pad_len]
def _evp_bytes_to_key(passphrase: bytes, salt: bytes, key_len: int, iv_len: int) -> tuple[bytes, bytes]:
d = b""
prev = b""
while len(d) < key_len + iv_len:
prev = hashlib.md5(prev + passphrase + salt).digest()
d += prev
return d[:key_len], d[key_len : key_len + iv_len]
def _json_dumps_like_js(value: Any) -> str:
return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
def ts_encrypt(input_value: Any, secret: str) -> str:
"""
Compatible with JS: CryptoJS.AES.encrypt(payload, secret).toString()
wrapped as RSA{<ciphertext>}.
"""
if secret is None or secret == "":
if isinstance(input_value, str):
return input_value
return _json_dumps_like_js(input_value)
payload = input_value
if not isinstance(input_value, str):
try:
payload = _json_dumps_like_js(input_value)
except Exception:
payload = str(input_value)
payload_bytes = payload.encode("utf-8")
salt = os.urandom(8)
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
encrypted = cipher.encrypt(_pkcs7_pad(payload_bytes, 16))
openssl_blob = b"Salted__" + salt + encrypted
b64 = base64.b64encode(openssl_blob).decode("utf-8")
return f"RSA{{{b64}}}"
def ts_decrypt(input_value: Any, secret: str) -> Any:
"""
Compatible with JS TS_decrypt behavior:
- If not string: return as-is.
- If RSA{...}: decrypt AES(CryptoJS passphrase mode), parse JSON when possible.
- If plain string JSON: parse JSON.
- Else: return raw string.
"""
if not isinstance(input_value, str):
return input_value
is_wrapped = input_value.startswith("RSA{") and input_value.endswith("}")
if is_wrapped:
if not secret:
raise TeleSecCryptoError("Secret is required to decrypt RSA payload")
b64 = input_value[4:-1]
try:
raw = base64.b64decode(b64)
except Exception as exc:
raise TeleSecCryptoError("Invalid base64 payload") from exc
if len(raw) < 16 or not raw.startswith(b"Salted__"):
raise TeleSecCryptoError("Unsupported encrypted payload format")
salt = raw[8:16]
ciphertext = raw[16:]
key, iv = _evp_bytes_to_key(secret.encode("utf-8"), salt, 32, 16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
decrypted = cipher.decrypt(ciphertext)
decrypted = _pkcs7_unpad(decrypted, 16)
try:
text = decrypted.decode("utf-8")
except UnicodeDecodeError:
text = decrypted.decode("latin-1")
try:
return json.loads(text)
except Exception:
return text
try:
return json.loads(input_value)
except Exception:
return input_value
@dataclass
class TeleSecDoc:
id: str
data: Any
raw: Dict[str, Any]
class TeleSecCouchDB:
"""
Direct CouchDB client for TeleSec docs (_id = "<table>:<id>").
No local replication layer.
"""
def __init__(
self,
server_url: str,
dbname: str,
secret: Optional[str] = None,
username: Optional[str] = None,
password: Optional[str] = None,
timeout: int = 30,
session: Optional[requests.Session] = None,
) -> None:
self.server_url = server_url.rstrip("/")
self.dbname = dbname
self.secret = secret or ""
self.timeout = timeout
self.base_url = f"{self.server_url}/{quote(self.dbname, safe='')}"
self.session = session or requests.Session()
self.session.headers.update({"Accept": "application/json"})
if username is not None:
self.session.auth = (username, password or "")
def _iso_now(self) -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def _doc_id(self, table: str, item_id: str) -> str:
return f"{table}:{item_id}"
def _request(self, method: str, path: str = "", **kwargs) -> requests.Response:
url = self.base_url if not path else f"{self.base_url}/{path.lstrip('/')}"
kwargs.setdefault("timeout", self.timeout)
res = self.session.request(method=method, url=url, **kwargs)
return res
def get_server_datetime(self) -> datetime:
"""
Returns server datetime using HTTP Date header from CouchDB.
Avoids reliance on local machine clock.
"""
candidates = [
("HEAD", self.base_url),
("GET", self.base_url),
("HEAD", self.server_url),
("GET", self.server_url),
]
for method, url in candidates:
try:
res = self.session.request(method=method, url=url, timeout=self.timeout)
date_header = res.headers.get("Date")
if not date_header:
continue
dt = email.utils.parsedate_to_datetime(date_header)
if dt is None:
continue
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
continue
raise TeleSecCouchDBError("Unable to retrieve server time from CouchDB Date header")
def iso_from_server_plus_minutes(self, minutes: int = 0) -> str:
now = self.get_server_datetime()
target = now.timestamp() + (minutes * 60)
out = datetime.fromtimestamp(target, tz=timezone.utc)
return out.isoformat(timespec="milliseconds").replace("+00:00", "Z")
def check_connection(self) -> Dict[str, Any]:
res = self._request("GET")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"CouchDB connection failed: {res.status_code} {res.text}")
return res.json()
def get_raw(self, doc_id: str) -> Optional[Dict[str, Any]]:
res = self._request("GET", quote(doc_id, safe=""))
if res.status_code == 404:
return None
if res.status_code >= 400:
raise TeleSecCouchDBError(f"GET doc failed: {res.status_code} {res.text}")
return res.json()
def put_raw(self, doc: Dict[str, Any]) -> Dict[str, Any]:
if "_id" not in doc:
raise ValueError("Document must include _id")
res = self._request(
"PUT",
quote(doc["_id"], safe=""),
headers={"Content-Type": "application/json"},
data=_json_dumps_like_js(doc).encode("utf-8"),
)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"PUT doc failed: {res.status_code} {res.text}")
return res.json()
def delete_raw(self, doc_id: str) -> bool:
doc = self.get_raw(doc_id)
if not doc:
return False
res = self._request("DELETE", f"{quote(doc_id, safe='')}?rev={quote(doc['_rev'], safe='')}")
if res.status_code >= 400:
raise TeleSecCouchDBError(f"DELETE doc failed: {res.status_code} {res.text}")
return True
def put(self, table: str, item_id: str, data: Any, encrypt: bool = True) -> Dict[str, Any]:
doc_id = self._doc_id(table, item_id)
if data is None:
self.delete_raw(doc_id)
return {"ok": True, "id": doc_id, "deleted": True}
existing = self.get_raw(doc_id)
doc: Dict[str, Any] = existing if existing else {"_id": doc_id}
to_store = data
is_encrypted_string = isinstance(data, str) and data.startswith("RSA{") and data.endswith("}")
if encrypt and self.secret and not is_encrypted_string:
to_store = ts_encrypt(data, self.secret)
doc["data"] = to_store
doc["table"] = table
doc["ts"] = self._iso_now()
return self.put_raw(doc)
def get(self, table: str, item_id: str, decrypt: bool = True) -> Optional[Any]:
doc_id = self._doc_id(table, item_id)
doc = self.get_raw(doc_id)
if not doc:
return None
value = doc.get("data")
if decrypt:
return ts_decrypt(value, self.secret)
return value
def delete(self, table: str, item_id: str) -> bool:
return self.delete_raw(self._doc_id(table, item_id))
def list(self, table: str, decrypt: bool = True) -> List[TeleSecDoc]:
params = {
"include_docs": "true",
"startkey": f'"{table}:"',
"endkey": f'"{table}:\uffff"',
}
res = self._request("GET", "_all_docs", params=params)
if res.status_code >= 400:
raise TeleSecCouchDBError(f"LIST docs failed: {res.status_code} {res.text}")
rows = res.json().get("rows", [])
out: List[TeleSecDoc] = []
for row in rows:
doc = row.get("doc") or {}
item_id = row.get("id", "").split(":", 1)[1] if ":" in row.get("id", "") else row.get("id", "")
value = doc.get("data")
if decrypt:
value = ts_decrypt(value, self.secret)
out.append(TeleSecDoc(id=item_id, data=value, raw=doc))
return out

176
python_sdk/windows_agent.py Normal file
View File

@@ -0,0 +1,176 @@
import argparse
import ctypes
import os
import socket
import subprocess
import sys
import time
from datetime import datetime, timezone
from typing import Any, Dict, Optional
import psutil
try:
from .telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
except ImportError:
from telesec_couchdb import TeleSecCouchDB, TeleSecCouchDBError, ts_decrypt
def utcnow_iso() -> str:
return datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z")
def parse_iso(value: str) -> Optional[datetime]:
if not value:
return None
try:
v = value.strip().replace("Z", "+00:00")
dt = datetime.fromisoformat(v)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
return dt.astimezone(timezone.utc)
except Exception:
return None
def get_current_username() -> str:
try:
return psutil.Process().username() or os.getlogin()
except Exception:
try:
return os.getlogin()
except Exception:
return os.environ.get("USERNAME", "")
def _window_title(hwnd: int) -> str:
buf_len = ctypes.windll.user32.GetWindowTextLengthW(hwnd)
if buf_len <= 0:
return ""
buf = ctypes.create_unicode_buffer(buf_len + 1)
ctypes.windll.user32.GetWindowTextW(hwnd, buf, buf_len + 1)
return buf.value or ""
def get_active_app() -> Dict[str, str]:
exe = ""
title = ""
try:
hwnd = ctypes.windll.user32.GetForegroundWindow()
if hwnd:
title = _window_title(hwnd)
pid = ctypes.c_ulong()
ctypes.windll.user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
if pid.value:
try:
proc = psutil.Process(pid.value)
exe = proc.name() or ""
except Exception:
exe = ""
except Exception:
pass
return {"exe": exe, "title": title}
def build_payload(machine_id: str) -> Dict[str, Any]:
app = get_active_app()
return {
"Hostname": machine_id,
"UsuarioActual": get_current_username(),
"AppActualEjecutable": app.get("exe", ""),
"AppActualTitulo": app.get("title", ""),
# campo local diagnóstico (no se usa para decisión remota)
"AgentLocalSeenAt": utcnow_iso(),
}
def should_shutdown(data: Dict[str, Any], server_now: datetime) -> bool:
target = parse_iso(str(data.get("ShutdownBeforeDate", "") or ""))
if not target:
return False
return server_now >= target
def execute_shutdown(dry_run: bool = False) -> None:
if dry_run:
print("[DRY-RUN] Ejecutaría: shutdown /s /t 0 /f")
return
subprocess.run(["shutdown", "/s", "/t", "0", "/f"], check=False)
def run_once(client: TeleSecCouchDB, machine_id: str, dry_run: bool = False) -> None:
server_now = client.get_server_datetime()
server_now_iso = server_now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
raw = client.get(table="aulas_ordenadores", item_id=machine_id, decrypt=False)
current: Dict[str, Any] = {}
if raw is not None:
current = ts_decrypt(raw, client.secret)
if not isinstance(current, dict):
current = {}
update = build_payload(machine_id)
update["LastSeenAt"] = server_now_iso
for key in ["ShutdownBeforeDate", "ShutdownRequestedAt", "ShutdownRequestedBy"]:
if key in current:
update[key] = current.get(key)
client.put(table="aulas_ordenadores", item_id=machine_id, data=update, encrypt=True)
if should_shutdown(update, server_now):
print(f"[{server_now_iso}] ShutdownBeforeDate alcanzado. Apagando {machine_id}...")
execute_shutdown(dry_run=dry_run)
else:
print(
f"[{server_now_iso}] Reportado {machine_id} user={update.get('UsuarioActual','')} "
f"exe={update.get('AppActualEjecutable','')} title={update.get('AppActualTitulo','')}"
)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="TeleSec Windows Agent")
parser.add_argument("--server", required=True, help="CouchDB server URL, ej. https://couch.example")
parser.add_argument("--db", default="telesec", help="Database name")
parser.add_argument("--user", default="", help="CouchDB username")
parser.add_argument("--password", default="", help="CouchDB password")
parser.add_argument("--secret", required=True, help="TeleSec secret para cifrado")
parser.add_argument("--machine-id", default="", help="ID de máquina (default: hostname)")
parser.add_argument("--interval", type=int, default=15, help="Intervalo en segundos")
parser.add_argument("--once", action="store_true", help="Ejecutar una sola iteración")
parser.add_argument("--dry-run", action="store_true", help="No apagar realmente, solo log")
return parser.parse_args()
def main() -> int:
args = parse_args()
machine_id = (args.machine_id or socket.gethostname() or "unknown-host").strip()
client = TeleSecCouchDB(
server_url=args.server,
dbname=args.db,
secret=args.secret,
username=args.user or None,
password=args.password or None,
)
try:
client.check_connection()
except TeleSecCouchDBError as exc:
print(f"Error de conexión CouchDB: {exc}", file=sys.stderr)
return 2
if args.once:
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
return 0
while True:
try:
run_once(client=client, machine_id=machine_id, dry_run=args.dry_run)
except Exception as exc:
print(f"Error en iteración agente: {exc}", file=sys.stderr)
time.sleep(max(5, args.interval))
if __name__ == "__main__":
raise SystemExit(main())

View File

@@ -1 +1,3 @@
requests
pycryptodome
psutil

View File

@@ -48,6 +48,9 @@ PAGES.aulas = {
<a class="button btn4" style="font-size: 25px;" href="#supercafe"
><img src="${PAGES.supercafe.icon}" height="20" /> Ver comandas</a
>
<a class="button btn8" style="font-size: 25px;" href="#aulas,ordenadores"
><img src="${PAGES.aulas.icon}" height="20" /> Control de ordenadores</a
>
</fieldset>
<fieldset style="float: left;">
<legend>Datos de hoy</legend>
@@ -451,6 +454,211 @@ Cargando...</pre
}
};
},
__decryptIfNeeded: function (table, id, raw) {
return new Promise((resolve) => {
if (typeof raw !== 'string') {
resolve(raw || {});
return;
}
TS_decrypt(
raw,
SECRET,
(data) => {
resolve(data || {});
},
table,
id
);
});
},
__getServerNow: async function () {
try {
var couchUrl = (localStorage.getItem('TELESEC_COUCH_URL') || '').replace(/\/$/, '');
var couchUser = localStorage.getItem('TELESEC_COUCH_USER') || '';
var couchPass = localStorage.getItem('TELESEC_COUCH_PASS') || '';
var couchDb = localStorage.getItem('TELESEC_COUCH_DBNAME') || 'telesec';
if (couchUrl) {
var target = couchUrl + '/' + encodeURIComponent(couchDb);
var headers = {};
if (couchUser) {
headers['Authorization'] = 'Basic ' + btoa(couchUser + ':' + couchPass);
}
var res = await fetch(target, { method: 'HEAD', headers: headers });
var dateHeader = res.headers.get('Date');
if (dateHeader) {
var dt = new Date(dateHeader);
if (!isNaN(dt.getTime())) return dt;
}
}
} catch (e) {
console.warn('No se pudo obtener hora desde CouchDB', e);
}
try {
var wres = await fetch('https://worldtimeapi.org/api/timezone/Etc/UTC');
if (wres.ok) {
var wjson = await wres.json();
if (wjson && wjson.utc_datetime) {
var wdt = new Date(wjson.utc_datetime);
if (!isNaN(wdt.getTime())) return wdt;
}
}
} catch (e2) {
console.warn('No se pudo obtener hora desde worldtimeapi', e2);
}
return new Date();
},
__scheduleShutdown: async function (machineId) {
try {
document.getElementById('actionStatus').style.display = 'block';
var serverNow = await PAGES.aulas.__getServerNow();
var shutdownAt = new Date(serverNow.getTime() + 2 * 60 * 1000).toISOString();
var raw = await DB.get('aulas_ordenadores', machineId);
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', machineId, raw);
data = data || {};
data.Hostname = data.Hostname || machineId;
data.ShutdownBeforeDate = shutdownAt;
data.ShutdownRequestedAt = serverNow.toISOString();
data.ShutdownRequestedBy = SUB_LOGGED_IN_ID || '';
await DB.put('aulas_ordenadores', machineId, data);
toastr.warning('Apagado programado antes de: ' + shutdownAt);
} catch (e) {
console.warn('Error programando apagado remoto', e);
toastr.error('No se pudo programar el apagado remoto');
} finally {
document.getElementById('actionStatus').style.display = 'none';
}
},
__cancelShutdown: async function (machineId) {
try {
document.getElementById('actionStatus').style.display = 'block';
var raw = await DB.get('aulas_ordenadores', machineId);
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', machineId, raw);
data = data || {};
data.Hostname = data.Hostname || machineId;
data.ShutdownBeforeDate = '';
data.ShutdownRequestedAt = '';
data.ShutdownRequestedBy = '';
await DB.put('aulas_ordenadores', machineId, data);
toastr.success('Apagado remoto cancelado');
} catch (e) {
console.warn('Error cancelando apagado remoto', e);
toastr.error('No se pudo cancelar el apagado remoto');
} finally {
document.getElementById('actionStatus').style.display = 'none';
}
},
_ordenadores: function () {
container.innerHTML = html`
<a class="button" href="#aulas">← Volver a Gestión de Aulas</a>
<h1>Control de ordenadores</h1>
<p>
Estado enviado por el agente Windows. El apagado remoto se programa con hora de servidor.
</p>
<div id="cont"></div>
`;
TS_IndexElement(
'aulas,ordenadores',
[
{ key: 'Hostname', type: 'raw', default: '', label: 'Hostname' },
{ key: 'UsuarioActual', type: 'raw', default: '', label: 'Usuario actual' },
{ key: 'AppActualEjecutable', type: 'raw', default: '', label: 'App actual (exe)' },
{ key: 'AppActualTitulo', type: 'raw', default: '', label: 'App actual (título)' },
{ key: 'LastSeenAt', type: 'raw', default: '', label: 'Último visto (server)' },
{
key: 'ShutdownBeforeDate',
type: 'template',
label: 'Apagado remoto',
template: (data, td) => {
var text = document.createElement('div');
text.style.marginBottom = '6px';
text.innerText = data.ShutdownBeforeDate
? '⏻ Antes de: ' + data.ShutdownBeforeDate
: 'Sin apagado programado';
td.appendChild(text);
var btnOn = document.createElement('button');
btnOn.className = 'rojo';
btnOn.innerText = 'Programar +2m';
btnOn.onclick = async (event) => {
event.preventDefault();
event.stopPropagation();
await PAGES.aulas.__scheduleShutdown(data._key);
return false;
};
td.appendChild(btnOn);
if (data.ShutdownBeforeDate) {
td.appendChild(document.createElement('br'));
var btnCancel = document.createElement('button');
btnCancel.className = 'btn5';
btnCancel.innerText = 'Cancelar';
btnCancel.onclick = async (event) => {
event.preventDefault();
event.stopPropagation();
await PAGES.aulas.__cancelShutdown(data._key);
return false;
};
td.appendChild(btnCancel);
}
},
},
],
'aulas_ordenadores',
document.querySelector('#cont')
);
},
_ordenadores__edit: function (mid) {
var field_host = safeuuid();
var field_user = safeuuid();
var field_exe = safeuuid();
var field_title = safeuuid();
var field_seen = safeuuid();
var field_shutdown = safeuuid();
var btn_schedule = safeuuid();
var btn_cancel = safeuuid();
container.innerHTML = html`
<a class="button" href="#aulas,ordenadores">← Volver a ordenadores</a>
<h1>Ordenador <code>${mid}</code></h1>
<fieldset style="float: none; width: calc(100% - 40px);max-width: none;">
<legend>Estado</legend>
<label>Hostname<br /><input readonly id="${field_host}" /></label><br /><br />
<label>Usuario actual<br /><input readonly id="${field_user}" /></label><br /><br />
<label>App actual (exe)<br /><input readonly id="${field_exe}" /></label><br /><br />
<label>App actual (título)<br /><input readonly id="${field_title}" /></label><br /><br />
<label>Último visto (server)<br /><input readonly id="${field_seen}" /></label><br /><br />
<label>ShutdownBeforeDate<br /><input readonly id="${field_shutdown}" /></label><br /><br />
<button class="rojo" id="${btn_schedule}">Programar apagado +2m</button>
<button class="btn5" id="${btn_cancel}">Cancelar apagado</button>
</fieldset>
`;
async function loadData() {
var raw = await DB.get('aulas_ordenadores', mid);
var data = await PAGES.aulas.__decryptIfNeeded('aulas_ordenadores', mid, raw);
data = data || {};
document.getElementById(field_host).value = data.Hostname || mid;
document.getElementById(field_user).value = data.UsuarioActual || '';
document.getElementById(field_exe).value = data.AppActualEjecutable || '';
document.getElementById(field_title).value = data.AppActualTitulo || '';
document.getElementById(field_seen).value = data.LastSeenAt || '';
document.getElementById(field_shutdown).value = data.ShutdownBeforeDate || '';
}
loadData();
document.getElementById(btn_schedule).onclick = async () => {
await PAGES.aulas.__scheduleShutdown(mid);
await loadData();
};
document.getElementById(btn_cancel).onclick = async () => {
await PAGES.aulas.__cancelShutdown(mid);
await loadData();
};
},
edit: function (fsection) {
if (!checkRole('aulas')) {
setUrlHash('index');
@@ -467,6 +675,9 @@ Cargando...</pre
case 'informes':
this._informes();
break;
case 'ordenadores':
this._ordenadores();
break;
default:
this.index();
break;
@@ -480,6 +691,9 @@ Cargando...</pre
case 'informes':
this._informes__edit(item);
break;
case 'ordenadores':
this._ordenadores__edit(item);
break;
}
}
},