CI / Build Native (push) Successful in 6m44s
El error 'window.api.updateCharacterWeight is not a function' se daba porque el browser tenía cacheada la versión vieja de api.js (que tenía updateCharacterInventory en vez de updateCharacterWeight). Agregar ?v=5 a los script src fuerza al browser a re-descargar el JS cuando el URL cambia (con cada nueva versión bump n). Previene errores 'is not a function' después de deploys que agregan/renombran métodos. Aplica a todos los archivos HTML que cargan JS estático.
191 lines
6.7 KiB
HTML
191 lines
6.7 KiB
HTML
<!doctype html>
|
|
<html lang="es" data-theme="light">
|
|
<head>
|
|
<meta charset="UTF-8"/>
|
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
|
<title>Usuarios — Calculadora de Craft de Shots</title>
|
|
<link rel="stylesheet" href="/static/css/pico.min.css"/>
|
|
<link rel="stylesheet" href="/static/css/app.css"/>
|
|
</head>
|
|
<body x-data="appShell()">
|
|
|
|
{#include partials/header.html /}
|
|
{#include partials/tab-bar.html /}
|
|
|
|
<main class="container" x-data="usuariosSection()">
|
|
|
|
<header class="flex-row justify-between mb-4">
|
|
<div>
|
|
<h2 class="text-bold" style="margin-bottom:0.25rem">Usuarios</h2>
|
|
<p class="text-sm text-muted">
|
|
Resetea la contrasena de cualquier usuario. Al resetear, debera
|
|
cambiarla en su proximo login.
|
|
</p>
|
|
</div>
|
|
<button type="button" @click="load()" :disabled="loading" class="secondary">
|
|
<span x-show="!loading">Refrescar</span>
|
|
<span x-show="loading">Cargando…</span>
|
|
</button>
|
|
</header>
|
|
|
|
<div x-show="error" class="error-msg mb-4" x-text="error"></div>
|
|
|
|
<div class="table-wrapper">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th class="cell-label text-left">Username</th>
|
|
<th class="cell-label text-left">Rol</th>
|
|
<th class="cell-label text-left">Estado</th>
|
|
<th class="cell-label text-left">Creado</th>
|
|
<th class="cell-label text-left">Ultimo login</th>
|
|
<th class="cell-label text-right">Acciones</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template x-for="u in users" :key="u.id">
|
|
<tr>
|
|
<td class="text-bold" x-text="u.username"></td>
|
|
<td>
|
|
<span x-show="u.isAdmin" class="badge badge-admin">admin</span>
|
|
<span x-show="!u.isAdmin" class="badge" style="background:var(--pico-muted-border-color);color:var(--pico-muted-color)">usuario</span>
|
|
</td>
|
|
<td>
|
|
<span x-show="u.mustChangePassword" class="badge badge-warning">cambiar pass</span>
|
|
<span x-show="!u.mustChangePassword" class="badge badge-success">ok</span>
|
|
</td>
|
|
<td class="text-mono text-xs" x-text="fmtDateTime(u.createdAt)"></td>
|
|
<td class="text-mono text-xs" x-text="u.lastLoginAt ? fmtDateTime(u.lastLoginAt) : '—'"></td>
|
|
<td class="text-right">
|
|
<button type="button"
|
|
@click="openReset(u)"
|
|
class="outline"
|
|
style="font-size:0.75rem;padding:0.25rem 0.5rem">
|
|
Resetear contrasena
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
<tr x-show="users.length === 0 && !loading">
|
|
<td colspan="6" class="text-center p-6 text-muted">Sin usuarios para listar.</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<div x-show="resetUser"
|
|
x-cloak
|
|
class="modal-backdrop">
|
|
<div class="modal-content" style="max-width:28rem">
|
|
<header style="padding:1.5rem 1.5rem 1rem;border-bottom:1px solid var(--pico-muted-border-color)">
|
|
<h2 style="font-size:1rem;margin-bottom:0.25rem"
|
|
x-text="'Resetear contrasena de ' + (resetUser?.username || '')"></h2>
|
|
<p class="text-sm text-muted">El usuario debera cambiar esta contrasena en su proximo login.</p>
|
|
</header>
|
|
<form @submit.prevent="submitReset()" style="padding:1.5rem">
|
|
<label>Nueva contrasena temporal
|
|
<input type="text"
|
|
x-model="resetPassword"
|
|
autofocus
|
|
required
|
|
minlength="8"/>
|
|
</label>
|
|
<div x-show="resetError" class="error-msg mt-2" x-text="resetError"></div>
|
|
<div class="flex-row justify-end mt-4" style="gap:0.5rem">
|
|
<button type="button" @click="closeReset()" :disabled="resetSubmitting" class="secondary">Cancelar</button>
|
|
<button type="submit" :disabled="resetSubmitting || resetPassword.length < 8" class="primary">
|
|
<span x-show="!resetSubmitting">Resetear</span>
|
|
<span x-show="resetSubmitting">Reseteando…</span>
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
</main>
|
|
|
|
<footer class="container">
|
|
<p class="text-center text-xs text-muted">
|
|
Auto-guardado activo · Cambios persistidos en el servidor cada ~500ms
|
|
</p>
|
|
</footer>
|
|
|
|
<script src="/static/js/defaults.js?v=5"></script>
|
|
<script src="/static/js/api.js?v=5"></script>
|
|
<script src="/static/js/app.js?v=5"></script>
|
|
<script defer src="/static/js/alpine.min.js?v=5"></script>
|
|
|
|
<script>
|
|
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('usuariosSection', () => ({
|
|
store() { return Alpine.store('app'); },
|
|
users: [],
|
|
loading: true,
|
|
error: null,
|
|
resetUser: null,
|
|
resetPassword: '',
|
|
resetSubmitting: false,
|
|
resetError: null,
|
|
|
|
async init() {
|
|
await this.load();
|
|
},
|
|
|
|
async load() {
|
|
this.loading = true;
|
|
this.error = null;
|
|
try {
|
|
const list = await window.api.adminListUsers();
|
|
this.users = list || [];
|
|
} catch (e) {
|
|
this.error = 'HTTP ' + (e.status || '') + ': ' + (e.message || 'Error');
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
openReset(user) {
|
|
this.resetUser = user;
|
|
this.resetPassword = '';
|
|
this.resetError = null;
|
|
this.resetSubmitting = false;
|
|
},
|
|
|
|
closeReset() {
|
|
this.resetUser = null;
|
|
this.resetPassword = '';
|
|
this.resetError = null;
|
|
},
|
|
|
|
async submitReset() {
|
|
if (!this.resetUser || this.resetPassword.length < 8) return;
|
|
this.resetSubmitting = true;
|
|
this.resetError = null;
|
|
try {
|
|
await window.api.adminResetPassword(this.resetUser.username, this.resetPassword);
|
|
this.closeReset();
|
|
await this.load();
|
|
} catch (e) {
|
|
this.resetError = 'HTTP ' + (e.status || '') + ': ' + (e.message || 'Error');
|
|
} finally {
|
|
this.resetSubmitting = false;
|
|
}
|
|
},
|
|
|
|
fmtDateTime(iso) {
|
|
if (!iso) return '';
|
|
const d = new Date(iso);
|
|
if (isNaN(d.getTime())) return iso;
|
|
return d.toLocaleString('es-ES', {
|
|
year: 'numeric', month: '2-digit', day: '2-digit',
|
|
hour: '2-digit', minute: '2-digit',
|
|
});
|
|
},
|
|
}));
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|