/**
* AdminUI โ admin dashboard.
*
* Two display modes:
* - modal: centered overlay above the game (opened via admin chip / ?admin URL)
* - page : fullscreen primary view (auto-shown when an admin signs in)
*
* Lets the admin view DB stats, every registered user, and:
* - reset a user's progress
* - toggle the admin flag
* - delete a user (except self)
* - export / import the user database as JSON
* - search by email or name
* - switch to the game view (page mode only)
*/
import { Auth } from '../auth/Auth.js';
export class AdminUI {
constructor({ onSwitchToGame } = {}) {
this._root = null;
this._mode = 'modal';
this._onSwitchToGame = onSwitchToGame;
}
/** Centred modal over the game (admin chip / ?admin URL). */
open() {
if (!Auth.isAdmin()) {
alert('Admin access only. Sign in with an admin account first.');
return false;
}
return this._show('modal');
}
/** Fullscreen primary view โ admin landing page after login. */
openAsPage() {
if (!Auth.isAdmin()) return false;
return this._show('page');
}
_show(mode) {
this._mode = mode;
if (this._root) { this._root.remove(); this._root = null; }
this._root = this._build();
document.body.appendChild(this._root);
this.render();
return true;
}
close() { if (this._root) { this._root.remove(); this._root = null; } }
_build() {
const isPage = this._mode === 'page';
const r = document.createElement('div');
r.id = 'adminPanel';
r.dataset.mode = this._mode;
const me = Auth.current();
r.innerHTML = `
| Email | Name | Role | Grade |
โญ Stars | Mastered | Created | Age | Actions |
No users match.
`;
r.style.cssText = isPage
? `position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;
display:flex;align-items:stretch;justify-content:center;
background:radial-gradient(ellipse at 50% 30%,#1a2440 0%,#070912 70%);padding:24px`
: `position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:99999;
display:flex;align-items:center;justify-content:center;
background:rgba(8,10,18,.85);backdrop-filter:blur(6px);padding:20px`;
if (isPage) {
r.querySelector('#admGame').onclick = () => this._switchToGame();
r.querySelector('#admSignOut').onclick = () => this._signOut();
} else {
r.querySelector('#admClose').onclick = () => this.close();
}
r.querySelector('#admNew').onclick = () => this._createUserPrompt();
r.querySelector('#admRefresh').onclick = () => this.render();
r.querySelector('#admExport').onclick = () => this._exportDb();
r.querySelector('#admImport').onclick = () => r.querySelector('#admImportFile').click();
r.querySelector('#admImportFile').onchange = (e) => this._importDb(e.target.files[0]);
r.querySelector('#admSearch').addEventListener('input', () => this.render());
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this._mode === 'modal' && this._root) this.close();
});
return r;
}
_switchToGame() {
if (!this._root) return;
this._root.remove();
this._root = null;
if (this._onSwitchToGame) this._onSwitchToGame();
}
async _createUserPrompt() {
const email = prompt('New user email:'); if (!email) return;
const name = prompt('Display name (optional):', email.split('@')[0]);
const pwd = prompt('Password (6+ chars):');
if (!pwd) return;
const isAdmin = confirm('Make this user an admin?');
try {
await Auth.adminCreateUser({ email, pwd, name, isAdmin });
alert(`Created ${email}${isAdmin ? ' (admin)' : ''}`);
this.render();
} catch (e) { alert('Could not create user: ' + e.message); }
}
async _signOut() {
if (!confirm('Sign out of the admin dashboard?')) return;
await Auth.logout().catch(() => {});
location.reload();
}
async render() {
if (!this._root) return;
let allUsers;
try { allUsers = await Auth.adminListUsers(); }
catch (e) { allUsers = Auth.allUsers(); }
const me = Auth.current()?.email;
// Stats โ always over the full database, regardless of search filter.
const totalStars = allUsers.reduce((s, u) => s + (u.stars || 0), 0);
const totalMastered = allUsers.reduce((s, u) => s + (u.mastered || 0), 0);
const admins = allUsers.filter(u => u.isAdmin).length;
const onlineCount = allUsers.filter(u => u.online).length;
this._root.querySelector('#admStats').innerHTML = `
${statCard('Total users', allUsers.length)}
${statCard('Online now', onlineCount, 'online-now')}
${statCard('Admins', admins)}
${statCard('โญ Stars', totalStars.toLocaleString())}
${statCard('Mastered', totalMastered.toLocaleString())}
`;
// Filter + sort.
const q = (this._root.querySelector('#admSearch').value || '').toLowerCase().trim();
const users = allUsers
.filter(u => !q || u.email.toLowerCase().includes(q) || (u.name || '').toLowerCase().includes(q))
.sort((a, b) => (b.stars || 0) - (a.stars || 0));
const body = this._root.querySelector('#admBody');
const empty = this._root.querySelector('#admEmpty');
body.innerHTML = '';
empty.style.display = users.length ? 'none' : 'block';
const now = Date.now();
for (const u of users) {
const tr = document.createElement('tr');
const isMe = u.email === me;
const ageDays = u.created ? Math.max(0, Math.floor((now - u.created) / 86400000)) : null;
tr.innerHTML = `