diff --git a/backend/.env.example b/backend/.env.example index 9b95c6a..d39bf3b 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -34,10 +34,10 @@ TRENDS_INTERVAL_HOURS=24 # CORS # Lista de orígenes permitidos separados por coma. -CORS_ORIGINS=http://localhost:5500,http://localhost:3000,http://127.0.0.1:5500 +CORS_ORIGINS=http://localhost:5500,http://localhost:3000,http://127.0.0.1:5500,http://127.0.0.1:5501 # Base URL del frontend. Este valor debe coincidir con la raíz real que sirve los archivos de frontend, y cambiará por completo al desplegar a producción. -FRONTEND_BASE_URL=http://localhost:5500/frontend +FRONTEND_BASE_URL=http://localhost:5500 # Autenticación del pipeline automatizado PIPELINE_TRIGGER_SECRET=reemplazar-por-cadena-aleatoria-segura diff --git a/docs/GUIA_GIT.md b/docs/GUIA_GIT.md index 3211378..4bc12d7 100644 --- a/docs/GUIA_GIT.md +++ b/docs/GUIA_GIT.md @@ -1,17 +1,12 @@ # Git en el dia a dia -Esta guia cubre los comandos de Git que usamos en SkillStat. No asume -conocimiento previo. Si ya dominas Git puedes usarla como referencia rapida. +Esta guia cubre los comandos de Git que usamos en SkillStat. No asume conocimiento previo. Si ya dominas Git puedes usarla como referencia rapida. ## Como funciona Git en este proyecto -Git guarda el historial de todos los cambios que hacemos al codigo. -Cada vez que guardamos un cambio con `commit` quedamos un registro de -que cambio, quien lo hizo y cuando. +Git guarda el historial de todos los cambios que hacemos al codigo. Cada vez que guardamos un cambio con `commit` quedamos un registro de que cambio, quien lo hizo y cuando. -Las ramas nos permiten trabajar en algo nuevo sin afectar lo que ya -funciona. Cuando terminamos, integramos nuestro trabajo a `develop` -a traves de un Pull Request en GitHub. +Las ramas nos permiten trabajar en algo nuevo sin afectar lo que ya funciona. Cuando terminamos, integramos nuestro trabajo a `develop` a traves de un Pull Request en GitHub. ## Los comandos que mas usamos @@ -107,5 +102,4 @@ Si queremos deshacer el ultimo commit pero conservar los cambios: git reset --soft HEAD~1 ``` -Si tenemos dudas sobre algo que no esta en esta guia lo preguntamos -antes de intentar comandos desconocidos. +Si tenemos dudas sobre algo que no esta en esta guia lo preguntamos antes de intentar comandos desconocidos. diff --git a/docs/LIMPIEZA_RESIDUALES.md b/docs/LIMPIEZA_RESIDUALES.md index 46009e1..fd13f0a 100644 --- a/docs/LIMPIEZA_RESIDUALES.md +++ b/docs/LIMPIEZA_RESIDUALES.md @@ -2,28 +2,19 @@ ## Contexto -Durante la auditoría técnica pre-Ronda 14 se detectaron cuatro archivos -no planificados en backend/ que fueron generados durante sesiones de -depuración manual y nunca formaron parte del scaffold del proyecto: +Durante la auditoría técnica pre-Ronda 14 se detectaron cuatro archivos no planificados en backend/ que fueron generados durante sesiones de depuración manual y nunca formaron parte del scaffold del proyecto: - backend/clean.py (80 líneas) - backend/clean2.py (79 líneas) - backend/clean3.py (78 líneas) - backend/test_read.py (13 líneas) -Los archivos fueron eliminados del working tree de develop durante -la sesión de diagnóstico. Esta rama los elimina formalmente del -historial mediante este registro de decisión. +Los archivos fueron eliminados del working tree de develop durante la sesión de diagnóstico. Esta rama los elimina formalmente del historial mediante este registro de decisión. ## Decisión -Se eliminan sin recuperación. No contenían lógica de aplicación, -pruebas formales ni configuración. Eran scripts ad-hoc de depuración -sin valor para el proyecto. +Se eliminan sin recuperación. No contenían lógica de aplicación, pruebas formales ni configuración. Eran scripts ad-hoc de depuración sin valor para el proyecto. ## Consecuencias -El directorio backend/ queda alineado con el scaffold original. -Cualquier utilidad de depuración futura debe crearse en backend/tests/ -o en scripts/ con nombre descriptivo y commitearse como parte del -flujo normal de desarrollo. +El directorio backend/ queda alineado con el scaffold original. Cualquier utilidad de depuración futura debe crearse en backend/tests/ o en scripts/ con nombre descriptivo y commitearse como parte del flujo normal de desarrollo. diff --git a/frontend/assets/css/components/_navbar.css b/frontend/assets/css/components/_navbar.css index 7a53834..7ba7cc3 100644 --- a/frontend/assets/css/components/_navbar.css +++ b/frontend/assets/css/components/_navbar.css @@ -248,6 +248,12 @@ width: 100%; } +.navbar__logout[hidden], +.navbar__profile-link[hidden], +.nav-drawer__logout[hidden] { + display: none; +} + @media (min-width: 768px) { .nav-drawer { display: none; diff --git a/frontend/assets/images/brand/favicon.ico b/frontend/assets/images/brand/favicon.ico index 65c0d0b..fc4eb45 100644 Binary files a/frontend/assets/images/brand/favicon.ico and b/frontend/assets/images/brand/favicon.ico differ diff --git a/frontend/assets/images/brand/favicon.svg b/frontend/assets/images/brand/favicon.svg index 22f0bb8..51a89a7 100644 --- a/frontend/assets/images/brand/favicon.svg +++ b/frontend/assets/images/brand/favicon.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/frontend/assets/images/brand/skillstat-logo-compact.svg b/frontend/assets/images/brand/skillstat-logo-compact.svg deleted file mode 100644 index 22f0bb8..0000000 --- a/frontend/assets/images/brand/skillstat-logo-compact.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/frontend/assets/images/brand/skillstat-logo.svg b/frontend/assets/images/brand/skillstat-logo.svg index 39a5a99..fb4bace 100644 --- a/frontend/assets/images/brand/skillstat-logo.svg +++ b/frontend/assets/images/brand/skillstat-logo.svg @@ -1,3 +1 @@ - - - + \ No newline at end of file diff --git a/frontend/assets/js/api/client.js b/frontend/assets/js/api/client.js index 5310773..b76ba46 100644 --- a/frontend/assets/js/api/client.js +++ b/frontend/assets/js/api/client.js @@ -54,7 +54,7 @@ async function ensureCsrfToken() { }); if (!response.ok) { - // Si no hay sesion activa (ej. durante login, register o google auth), el endpoint devuelve 401. No es una falla critica — retornamos null para que la peticion original continue sin el header CSRF, tal como funciona correctamente para los endpoints que no tienen @jwt_required(). + // Si no hay sesion activa (ej. durante login, register o google auth), el endpoint devuelve 401. No es una falla critica, retornamos null para que la peticion original continue sin el header CSRF, tal como funciona correctamente para los endpoints que no tienen @jwt_required(). console.warn( "[ensureCsrfToken] No se pudo obtener el token CSRF (sin sesion activa), continuando sin el header.", ); @@ -141,3 +141,34 @@ async function apiDelete(endpoint) { return null; } } + +const GUEST_INTENT_KEY = "skillstat_guest_intent"; +const GUEST_INTENT_TTL_MS = 30 * 60 * 1000; + +function saveGuestIntent(returnTo, action) { + const intent = { returnTo, action: action || null, ts: Date.now() }; + localStorage.setItem(GUEST_INTENT_KEY, JSON.stringify(intent)); +} + +async function resolvePostLoginRedirect(defaultUrl) { + const raw = localStorage.getItem(GUEST_INTENT_KEY); + localStorage.removeItem(GUEST_INTENT_KEY); + if (!raw) return defaultUrl; + let intent; + try { + intent = JSON.parse(raw); + } catch { + return defaultUrl; + } + if (!intent || Date.now() - intent.ts > GUEST_INTENT_TTL_MS) { + return defaultUrl; + } + if (intent.action && intent.action.type === "add-skill") { + try { + await apiPost("/profile/skills", { skill_id: intent.action.skillId }); + } catch (e) { + console.warn("No se pudo completar la acción pendiente del invitado:", e); + } + } + return intent.returnTo || defaultUrl; +} diff --git a/frontend/assets/js/components/google-auth.init.js b/frontend/assets/js/components/google-auth.init.js index 0b5eff9..a607266 100644 --- a/frontend/assets/js/components/google-auth.init.js +++ b/frontend/assets/js/components/google-auth.init.js @@ -45,7 +45,9 @@ function handleGoogleCredential(response) { modal.hidden = false; } } else { - window.location.href = "/views/panorama.html"; + resolvePostLoginRedirect("/views/panorama.html").then((target) => { + window.location.href = target; + }); } }) .catch((error) => { @@ -105,8 +107,9 @@ function initGoogleAuth() { if (modalError) modalError.hidden = true; apiPost("/auth/google/confirm-link", { link_token: pendingLinkToken }) - .then(() => { - window.location.href = "/views/panorama.html"; + .then(() => resolvePostLoginRedirect("/views/panorama.html")) + .then((target) => { + window.location.href = target; }) .catch((error) => { btnConfirm.textContent = originalText; diff --git a/frontend/assets/js/components/navbar-role.js b/frontend/assets/js/components/navbar-role.js index 052280c..db7622f 100644 --- a/frontend/assets/js/components/navbar-role.js +++ b/frontend/assets/js/components/navbar-role.js @@ -3,7 +3,17 @@ let user; try { user = await apiGet("/profile/me"); + document + .querySelectorAll( + ".navbar__profile-link, .navbar__logout, .nav-drawer__logout", + ) + .forEach(function (el) { + el.removeAttribute("hidden"); + }); } catch { + document.querySelectorAll("[data-guest-cta]").forEach(function (el) { + el.removeAttribute("hidden"); + }); // Si /profile/me falla (401, red, lo que sea), simplemente no se inyecta el link. Cada página ya maneja su propio guard de sesión por separado; este script solo decide visibilidad. return; } diff --git a/frontend/assets/js/pages/alertas.js b/frontend/assets/js/pages/alertas.js index 28f6dfe..ae9a6d0 100644 --- a/frontend/assets/js/pages/alertas.js +++ b/frontend/assets/js/pages/alertas.js @@ -216,6 +216,7 @@ async function initAlertasPage() { bindCreateForm(); } catch (error) { if (error.status === 401) { + saveGuestIntent(window.location.pathname, null); window.location.href = "/views/register.html"; } else { console.error("Error cargando alertas:", error); diff --git a/frontend/assets/js/pages/habilidades.js b/frontend/assets/js/pages/habilidades.js index d481e0a..5e3fa3f 100644 --- a/frontend/assets/js/pages/habilidades.js +++ b/frontend/assets/js/pages/habilidades.js @@ -4,6 +4,7 @@ let activeDemand = "all"; let searchQuery = ""; let maxDemand = 0; let demandChecks = {}; +let userSkillIds = new Set(); async function initHabilidadesPage() { const loadingEl = document.getElementById("habilidades-loading"); @@ -17,6 +18,15 @@ async function initHabilidadesPage() { maxDemand = Math.max(...skills.map((s) => s.demand_count)); } + try { + const gapData = await apiGet("/profile/skill-gap"); + if (gapData && gapData.mis_habilidades) { + gapData.mis_habilidades.forEach((s) => userSkillIds.add(s.skill_id)); + } + } catch (e) { + // Ignoramos silenciosamente (guest o error de red) + } + loadingEl.hidden = true; setupFilters(skills); @@ -194,9 +204,11 @@ function renderSkills() {
- + ${ + userSkillIds.has(skill.skill_id) + ? `` + : `` + }
`; @@ -228,6 +240,10 @@ async function handleAddSkill(skillId, btnElement) { console.error("Error agregando habilidad:", error); if (error.status === 401) { btnElement.textContent = "Inicia sesión para guardar"; + saveGuestIntent(window.location.pathname, { + type: "add-skill", + skillId: parseInt(skillId), + }); setTimeout(() => { window.location.href = "/views/register.html"; }, 1000); diff --git a/frontend/assets/js/pages/register.js b/frontend/assets/js/pages/register.js index a68832f..ed7b36b 100644 --- a/frontend/assets/js/pages/register.js +++ b/frontend/assets/js/pages/register.js @@ -200,7 +200,8 @@ async function handleLoginSubmit(event) { email: form.email.value, password: form.password.value, }); - window.location.href = "/views/panorama.html"; + const target = await resolvePostLoginRedirect("/views/panorama.html"); + window.location.href = target; } catch (error) { if (error.code === "EMAIL_NOT_VERIFIED") { showUnverifiedBanner(form, form.email.value); diff --git a/frontend/views/admin/respaldos.html b/frontend/views/admin/respaldos.html index 521592a..75bbee7 100644 --- a/frontend/views/admin/respaldos.html +++ b/frontend/views/admin/respaldos.html @@ -40,27 +40,29 @@