Files
portail-santinova/index.html
Santinova Deployment bd00148615
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 10s
fix(portal): deduplicate tiles and align FALC visual
2026-10-25 22:14:30 +00:00

92 lines
16 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Portail Applicatif — Santinova Soft</title>
<style>
:root { --accent: #4f46e5; --accent-soft: rgba(79,70,229,.16); --accent-border: rgba(129,140,248,.42); }
* { box-sizing: border-box; margin: 0; padding: 0; }
body { min-height: 100vh; display: flex; flex-direction: column; background: #0d0f16; color: #edf0fa; font-family: "Segoe UI", system-ui, sans-serif; }
header { padding: 28px clamp(24px,5vw,64px); background: linear-gradient(118deg,#171b30,#252b50 55%,#171a2c); border-bottom: 1px solid rgba(255,255,255,.09); box-shadow: 0 7px 28px rgba(0,0,0,.28); }
.header-inner { max-width: 1240px; margin: auto; display: flex; align-items: center; justify-content: space-between; gap: 24px; }
.brand { display: flex; align-items: center; gap: 16px; min-width: 0; }
.brand-mark { width: 46px; height: 46px; display: grid; place-items: center; flex: 0 0 46px; border-radius: 14px; color: white; background: linear-gradient(135deg,var(--accent),#171b39); box-shadow: 0 8px 21px var(--accent-soft); }
.brand-title { font-size: clamp(1.24rem,2vw,1.55rem); font-weight: 750; letter-spacing: -.35px; }
.brand-subtitle { margin-top: 3px; color: #aeb5cf; font-size: .73rem; font-weight: 700; letter-spacing: 1.05px; text-transform: uppercase; }
.environment-badge { padding: 7px 13px; border: 1px solid var(--accent-border); border-radius: 999px; color: #f0f2ff; background: var(--accent-soft); font-size: .75rem; font-weight: 800; white-space: nowrap; }
.portal-hub-nav { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 9px; padding: 13px clamp(24px,5vw,64px); background: #141728; border-bottom: 1px solid rgba(255,255,255,.07); }
.portal-hub-label { margin-right: 5px; color: #7f88a9; font-size: .72rem; font-weight: 800; letter-spacing: .9px; text-transform: uppercase; }
.portal-hub-link { display: inline-flex; align-items: center; min-height: 36px; padding: 0 13px; border: 1px solid rgba(255,255,255,.11); border-radius: 10px; color: #dce2f5; background: rgba(255,255,255,.035); font-size: .82rem; font-weight: 700; text-decoration: none; transition: transform .16s ease-out,background .16s ease-out,border-color .16s ease-out; }
.portal-hub-link:hover { transform: translateY(-1px); color: #fff; background: var(--accent-soft); border-color: var(--accent-border); }
.app-card:focus-visible { outline: 3px solid white; outline-offset: 3px; }
main { width: 100%; max-width: 1240px; margin: auto; padding: 58px clamp(24px,5vw,64px) 72px; flex: 1; }
.section-header { max-width: 680px; margin: 0 auto 42px; text-align: center; }
.section-header h1 { font-size: clamp(2rem,4.5vw,3rem); letter-spacing: -1.1px; line-height: 1.08; }
.section-header p { margin-top: 12px; color: #aeb5cb; font-size: 1rem; line-height: 1.55; }
.count { display: inline-flex; align-items: center; justify-content: center; min-width: 27px; height: 27px; margin-left: 9px; vertical-align: middle; border: 1px solid var(--accent-border); border-radius: 999px; background: var(--accent-soft); font-size: .76rem; }
.apps-grid { display: grid; grid-template-columns: repeat(auto-fill,minmax(210px,1fr)); gap: 28px; align-items: start; }
.app-card { display: flex; flex-direction: column; align-items: center; width: 100%; max-width: 250px; justify-self: center; color: inherit; text-decoration: none; transition: transform .2s cubic-bezier(.23,1,.32,1),opacity .2s ease,filter .2s ease; }
.app-card:hover { transform: translateY(-7px); }.app-card:active { transform: scale(.975); }
.card-image-wrapper { position: relative; width: 180px; height: 180px; overflow: hidden; border: 1px solid rgba(255,255,255,.1); border-radius: 34px; background: #22263b; box-shadow: 0 10px 28px rgba(0,0,0,.34); transition: box-shadow .2s ease; }
.app-card:hover .card-image-wrapper { box-shadow: 0 19px 44px rgba(0,0,0,.42),0 0 0 1px var(--accent-border); }
.card-image-wrapper img { width: 100%; height: 100%; display: block; object-fit: cover; }.card-image-wrapper:after { content:""; position:absolute; inset:0; border-radius:inherit; box-shadow:inset 0 0 0 1px rgba(255,255,255,.08); pointer-events:none; }
.card-name { margin-top: 15px; color: #d9deed; text-align: center; font-size: .96rem; font-weight: 700; line-height: 1.35; }.card-env-badge,.card-status-badge { margin-top: 7px; padding: 3px 9px; border-radius: 999px; font-size: .66rem; font-weight: 800; letter-spacing: .85px; text-transform: uppercase; }
.card-env-badge { border: 1px solid var(--accent-border); color:#d5dbfb; background:var(--accent-soft); }.card-status-badge { display:none; border:1px solid rgba(248,113,113,.42); color:#fecaca; background:rgba(239,68,68,.12); }
.app-card.offline { cursor:not-allowed; opacity:.42; pointer-events:none; filter:grayscale(85%); }.app-card.offline .card-status-badge { display:inline-flex; }.app-card.offline .card-image-wrapper { box-shadow:none; }
.loading-msg { grid-column: 1 / -1; padding:64px 0; color:#aeb5cb; text-align:center; }.loading-msg.error { color:#fecaca; }
footer { padding:24px clamp(24px,5vw,64px); border-top:1px solid rgba(255,255,255,.06); background:#0a0c13; }.footer-inner { max-width:1240px; margin:auto; display:flex; align-items:center; justify-content:center; gap:9px; color:#737c99; font-size:.78rem; }.footer-inner img { height:50px; width:auto; object-fit:contain; }
@media (prefers-reduced-motion: reduce) { *,*:before,*:after { transition:none!important; } }
@media (max-width:640px) { header{padding-top:22px;padding-bottom:22px}.header-inner{align-items:flex-start}.environment-badge{margin-top:4px}.portal-hub-nav{justify-content:flex-start;padding-top:10px;padding-bottom:10px}.portal-hub-label{width:100%;margin-right:0}.portal-hub-link{min-height:34px;padding:0 10px;font-size:.76rem}main{padding-top:42px}.apps-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:23px 14px}.card-image-wrapper{width:min(100%,145px);height:145px;border-radius:27px} }
</style>
</head>
<body>
<header><div class="header-inner"><div class="brand"><div class="brand-mark" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/><rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/></svg></div><div><div class="brand-title" id="portal-title">Portail Applicatif</div><div class="brand-subtitle" id="portal-subtitle">Chargement de l'environnement</div></div></div><div class="environment-badge" id="environment-badge">…</div></div></header>
<nav class="portal-hub-nav" id="portal-hub-nav" aria-label="Accès aux portails d'entité" hidden></nav>
<main><section class="section-header" aria-labelledby="entity-heading"><h1 id="entity-heading">Chargement du portail<span class="count" id="app-count">…</span></h1><p id="entity-description">Les applications de cet environnement sont chargées depuis le dashboard correspondant.</p></section><section id="apps-grid" class="apps-grid" aria-live="polite"><div class="loading-msg">Chargement des applications…</div></section></main>
<footer><div class="footer-inner"><span>powered by</span><img src="images/logo_santinova.webp" alt="Santinova" /></div></footer>
<script>
/* Le nom de domaine est la source d'autorité : il définit l'entité, le dashboard et l'environnement. */
var PORTAL_CONFIGS = {
'portail-itinova.recette.santinova-soft.org': { env: 'recette', group: 'ITINOVA', title: 'Portail Itinova', accent: '#4f46e5' },
'portail-soins-et-sante.recette.santinova-soft.org': { env: 'recette', group: 'SOINS_ET_SANTE', title: 'Portail Soins et Santé', accent: '#0f766e' },
'portail-santinova.recette.santinova-soft.org': { env: 'recette', group: 'SANTINOVA', title: 'Portail Santinova', accent: '#7c3aed' },
'portail-santelis.recette.santinova-soft.org': { env: 'recette', group: 'SANTELIS', title: 'Portail SANTELIS', accent: '#15803d' },
'portail-itinova.santinova-soft.org': { env: 'prod', group: 'ITINOVA', title: 'Portail Itinova', accent: '#4f46e5' },
'portail-soins-et-sante.santinova-soft.org': { env: 'prod', group: 'SOINS_ET_SANTE', title: 'Portail Soins et Santé', accent: '#0f766e' },
'portail-santinova.santinova-soft.org': { env: 'prod', group: 'SANTINOVA', title: 'Portail Santinova', accent: '#7c3aed' },
'portail-santelis.santinova-soft.org': { env: 'prod', group: 'SANTELIS', title: 'Portail SANTELIS', accent: '#15803d' }
};
/* Le portail Santinova est le hub unique : les autres portails n’exposent pas cette navigation. */
var PORTAL_HUB_LINKS = [
{ label: 'Portail Itinova', host: 'portail-itinova' },
{ label: 'Portail Soins et Santé', host: 'portail-soins-et-sante' },
{ label: 'Portail SANTELIS', host: 'portail-santelis' }
];
/* SANTELIS Gestion est déployée uniquement en production et volontairement hors inventaire Dashboard. */
var PORTAL_ONLY_APPS = [{ id:'santelis-gestion', name:'SANTELIS Gestion', urls:{ prod:'https://gestion-santelis.santinova-soft.org' }, image:'images/santelis-gestion.jpg' }];
var STATUS_REFRESH_MS = 30000, APPS_REFRESH_MS = 300000, APP_IMAGE_OVERRIDES = { 'santelis-gestion': 'images/santelis-gestion.jpg' }, hasRenderedApps = false;
function normalizeHostname(value) { return String(value || '').toLowerCase().replace(/\.$/, ''); }
function getPortalHubUrl(portal) { return 'https://' + portal.host + (PORTAL.env === 'recette' ? '.recette' : '') + '.santinova-soft.org/'; }
function resolvePortalConfig(hostname) { var config = PORTAL_CONFIGS[normalizeHostname(hostname)]; if (!config) return null; var origin = config.env === 'recette' ? 'https://dashboard-santinova.recette.santinova-soft.org' : 'https://dashboard-santinova.santinova-soft.org'; return Object.assign({}, config, { dashboardAppsUrl: origin + '/api/public/apps', dashboardStatusUrl: origin + '/api/public/status', envLabel: config.env === 'recette' ? 'Recette' : 'Production' }); }
function isHttpsUrl(value) { try { return new URL(value).protocol === 'https:'; } catch (error) { return false; } }
function isOffline(app) { return app.status === 'offline' || app.status === 'error' || app.containerRunning === false; }
function getPortalOnlyApps() { if (!PORTAL || PORTAL.env !== 'prod' || PORTAL.group !== 'SANTELIS') return []; return PORTAL_ONLY_APPS.filter(function(app) { return isHttpsUrl(app.urls && app.urls[PORTAL.env]); }); }
function isAppInCurrentPortal(app) { if (!PORTAL || !app || !isHttpsUrl(app.urls && app.urls[PORTAL.env])) return false; if (app.id === 'santelis-gestion' || app.id === 'portail-santinova') return false; if (PORTAL.group === 'SANTINOVA') return app.category === 'SANTINOVA' || app.category === 'INFRA'; return app.category === PORTAL.group; }
/* Le Dashboard doit fournir des identifiants uniques ; le portail les protège aussi afin qu’un inventaire transitoirement erroné ne duplique jamais une tuile. */
function uniqueAppsById(apps) { var seen = new Set(); return (Array.isArray(apps) ? apps : []).filter(function(app) { if (!app || typeof app.id !== 'string' || !app.id || seen.has(app.id)) return false; seen.add(app.id); return true; }); }
function escapeHtml(value) { return String(value || '').replace(/[&<>'"]/g, function(character) { return ({ '&':'&amp;', '<':'&lt;', '>':'&gt;', "'":'&#39;', '"':'&quot;' })[character]; }); }
function buildCard(app) { var url = app.urls && app.urls[PORTAL.env], image = APP_IMAGE_OVERRIDES[app.id] || app.image || ('images/' + app.id + '.png'), offline = isOffline(app); return '<a class="app-card' + (offline ? ' offline' : '') + '" data-app-id="' + escapeHtml(app.id) + '" href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer"' + (offline ? ' aria-disabled="true"' : '') + '><div class="card-image-wrapper"><img src="' + escapeHtml(image) + '" alt="' + escapeHtml(app.name) + '" onerror="this.style.display=\'none\'" /></div><div class="card-name">' + escapeHtml(app.name) + '</div><div class="card-env-badge">' + PORTAL.envLabel + '</div><div class="card-status-badge">Hors ligne</div></a>'; }
function renderPortalHubNavigation() { var navigation = document.getElementById('portal-hub-nav'); if (!PORTAL || PORTAL.group !== 'SANTINOVA') { navigation.innerHTML = ''; navigation.hidden = true; return; } navigation.innerHTML = '<span class="portal-hub-label">Accès aux portails</span>' + PORTAL_HUB_LINKS.map(function(portal) { return '<a class="portal-hub-link" href="' + getPortalHubUrl(portal) + '">' + portal.label + '</a>'; }).join(''); navigation.hidden = false; }
function setPortalIdentity() { document.documentElement.style.setProperty('--accent', PORTAL.accent); document.title = PORTAL.title + ' — Santinova Soft'; document.getElementById('portal-title').textContent = PORTAL.title; document.getElementById('portal-subtitle').textContent = 'Environnement de ' + PORTAL.envLabel.toLowerCase(); document.getElementById('environment-badge').textContent = PORTAL.envLabel; document.getElementById('entity-heading').firstChild.nodeValue = PORTAL.title; document.getElementById('entity-description').textContent = 'Applications déployées dans l’environnement ' + PORTAL.envLabel.toLowerCase() + '.'; renderPortalHubNavigation(); }
function renderApps(apps) { var visibleApps = uniqueAppsById(apps).filter(isAppInCurrentPortal).concat(getPortalOnlyApps()), grid = document.getElementById('apps-grid'); grid.innerHTML = visibleApps.length ? visibleApps.map(buildCard).join('') : '<div class="loading-msg">Aucune application déployée pour ce portail.</div>'; document.getElementById('app-count').textContent = visibleApps.length; hasRenderedApps = true; }
async function requestJson(url) { var controller = new AbortController(), timeoutId = setTimeout(function() { controller.abort(); }, 8000); try { var response = await fetch(url, { cache:'no-store', signal:controller.signal }); if (!response.ok) throw new Error('HTTP ' + response.status); return await response.json(); } finally { clearTimeout(timeoutId); } }
async function fetchAndApplyStatuses() { try { var statuses = await requestJson(PORTAL.dashboardStatusUrl); if (!Array.isArray(statuses)) return; var byId = new Map(statuses.map(function(status) { return [status.id,status]; })); document.querySelectorAll('.app-card[data-app-id]').forEach(function(card) { var status = byId.get(card.getAttribute('data-app-id')); if (!status) return; var offline = isOffline(status); card.classList.toggle('offline', offline); card.setAttribute('aria-disabled', String(offline)); }); } catch (error) { console.warn('[Portail] Mise à jour des statuts impossible :', error.message); } }
async function loadApps() { try { var apps = await requestJson(PORTAL.dashboardAppsUrl); if (!Array.isArray(apps)) throw new Error('réponse applications invalide'); renderApps(apps); await fetchAndApplyStatuses(); } catch (error) { console.warn('[Portail] Chargement des applications impossible :', error.message); if (!hasRenderedApps) { document.getElementById('apps-grid').innerHTML = '<div class="loading-msg error">Dashboard indisponible — veuillez réessayer.</div>'; document.getElementById('app-count').textContent = '!'; } } }
var CURRENT_HOST = normalizeHostname(window.location.hostname), PORTAL = resolvePortalConfig(CURRENT_HOST);
if (!PORTAL) document.addEventListener('DOMContentLoaded', function() { document.getElementById('apps-grid').innerHTML = '<div class="loading-msg error">Nom de portail non reconnu.</div>'; });
else document.addEventListener('DOMContentLoaded', function() { setPortalIdentity(); loadApps(); setInterval(fetchAndApplyStatuses, STATUS_REFRESH_MS); setInterval(loadApps, APPS_REFRESH_MS); });
</script>
</body>
</html>