Initial commit — multi-tenant filtering, port constraints, chart bbox

This commit is contained in:
2026-05-04 22:41:09 -04:00
parent c3b07be67e
commit fcf1d2787a
1102 changed files with 7353 additions and 1166 deletions
+94 -21
View File
@@ -1,6 +1,6 @@
'use strict';
const API = 'http://localhost:5503';
const API = ''; // relative — works from any IP (localhost, Tailscale, LAN)
const SESSION_KEY = 'ams_session';
// ── Estado de sesión ────────────────────────────────────────────────────────
@@ -51,7 +51,18 @@ const Auth = {
`;
el.title = 'Click to logout';
el.onclick = () => {
if (confirm(`Logout as ${this.session.nombre}?`)) Auth.clear();
if (confirm(`Logout as ${this.session.nombre}?`)) {
Auth.clear();
window._clearPortConstraints?.();
_portCache = null;
// Return to login screen
document.getElementById('app').classList.add('hidden');
document.getElementById('login-screen').classList.remove('hidden');
document.getElementById('ls-user').value = '';
document.getElementById('ls-pass').value = '';
document.getElementById('ls-error').classList.add('hidden');
setTimeout(() => document.getElementById('ls-user').focus(), 100);
}
};
}
},
@@ -97,15 +108,11 @@ const Modal = {
if (!res.ok) throw new Error('invalid');
const data = await res.json();
if (!['ADMIN','SUPERADMIN'].includes(data.role)) {
errEl.textContent = 'Your account does not have edit permissions.';
errEl.classList.remove('hidden');
return;
}
Auth.save({ token: data.access_token, username: data.username,
nombre: data.nombre, role: data.role });
_hideOverlay();
afterLogin();
_fetchAndApplyPort(data.access_token, data.role); // update constraints if user changed
if (this._pendingAid) {
this.openEdit(this._pendingAid);
@@ -360,6 +367,61 @@ function _hideOverlay() {
document.getElementById('modal-edit').classList.add('hidden');
}
// ── Port navigation + constraints ─────────────────────────────────────────────
// Cached port result — re-applied after the app container becomes visible.
let _portCache = null; // { lon, lat, zoom, port }
// Called right after #app becomes visible.
// OL initialized with display:none → size=0×0 → map renders half-screen and
// ignores setCenter. Fix: updateSize() tells OL the real container size,
// then re-apply port position so the map lands at the correct place.
function _showMap() {
requestAnimationFrame(() => {
window._mapUpdateSize?.(); // fix half-screen rendering
requestAnimationFrame(() => {
_applyPortToMap(); // re-apply position after resize
});
});
}
function _applyPortToMap() {
if (!_portCache) return;
const { lon, lat, zoom, port } = _portCache;
// Step 1: clear any previous constraints so navigation is free
window._clearPortConstraints?.();
// Step 2: fly to port (same as port search)
window.flyToCoords?.(lon, lat, zoom);
// Step 3: after animation lands, lock zoom-out and extent
if (port) {
setTimeout(() => window._setPortConstraints?.(port), 1400); // 1200ms anim + margin
}
}
// Fetch /org/me/company, cache result, apply to map.
// Call this BEFORE showing the app, then re-apply after show (OL resize fix).
async function _fetchAndApplyPort(token, role) {
try {
const res = await fetch(`${API}/org/me/company`, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) return;
const me = await res.json();
if (!me.port || me.port.center_lat == null || me.port.center_lon == null) return;
const isClient = (role === 'USER' || role === 'CLIENT_ADMIN');
_portCache = {
lon: me.port.center_lon,
lat: me.port.center_lat,
zoom: me.port.default_zoom || 12,
port: isClient ? me.port : null,
};
_applyPortToMap();
} catch (_) { /* non-fatal */ }
}
// ── Login de inicio ───────────────────────────────────────────────────────────
async function initStartupLogin() {
const screen = document.getElementById('login-screen');
@@ -367,9 +429,7 @@ async function initStartupLogin() {
const btn = document.getElementById('ls-submit');
const errEl = document.getElementById('ls-error');
// Validate the cached token against /auth/me before auto-entering. The
// backend may have restarted / token may have expired — in that case we
// force the user back to the login screen instead of showing a broken UI.
// Validate cached token. If valid, configure map THEN show app.
Auth.load();
if (Auth.isLoggedIn()) {
try {
@@ -377,18 +437,22 @@ async function initStartupLogin() {
headers: { Authorization: `Bearer ${Auth.token()}` },
});
if (r.ok) {
try {
await _fetchAndApplyPort(Auth.token(), Auth.session.role);
app.classList.remove('hidden');
window._mapUpdateSize?.();
_applyPortToMap();
} catch (e) {
console.warn('[auth] map positioning error:', e);
app.classList.remove('hidden');
}
screen.classList.add('hidden');
app.classList.remove('hidden');
afterLogin();
return;
}
// 401 / network error → drop the stale session and show login
Auth.clear();
} catch {
Auth.clear();
}
} catch { Auth.clear(); }
}
// Make sure login screen is visible if we got here
screen.classList.remove('hidden');
app.classList.add('hidden');
@@ -412,8 +476,18 @@ async function initStartupLogin() {
const data = await res.json();
Auth.save({ token: data.access_token, username: data.username,
nombre: data.nombre, role: data.role });
screen.classList.add('hidden');
app.classList.remove('hidden');
// Map positioning is separate — errors here must NOT show credential message
try {
await _fetchAndApplyPort(data.access_token, data.role);
app.classList.remove('hidden'); // show app BEHIND login screen (z-index:9999)
window._mapUpdateSize?.(); // OL measures real container size
_applyPortToMap(); // position map at correct port
} catch (e) {
console.warn('[auth] map positioning error:', e);
app.classList.remove('hidden'); // show app anyway at default position
}
screen.classList.add('hidden'); // reveal map
afterLogin();
} catch {
errEl.textContent = 'Invalid username or password.';
@@ -425,7 +499,7 @@ async function initStartupLogin() {
}
btn.addEventListener('click', doLogin);
document.getElementById('ls-pass').addEventListener('keydown', (e) => {
document.getElementById('ls-pass').addEventListener('keydown', e => {
if (e.key === 'Enter') doLogin();
});
}
@@ -433,7 +507,6 @@ async function initStartupLogin() {
function afterLogin() {
if (window.PortSearch) window.PortSearch.init();
Auth._renderBadge();
// Sincronizar barra de estado inferior con usuario conectado
const sbUser = document.getElementById('sb-user');
if (sbUser && Auth.session) {
sbUser.textContent = `${Auth.session.nombre} · ${Auth.session.role}`;