Files
portail-santinova/index.html
manus-admin e2ce31b555
All checks were successful
Validation applicative / Tests de contrat (push) Successful in 14s
fix: isoler Santelis hors inventaire dashboard
2026-10-24 09:27:58 +00:00

88 lines
15 KiB
HTML
Raw 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; }
.entity-nav { padding: 13px clamp(24px,5vw,64px); background: #141728; border-bottom: 1px solid rgba(255,255,255,.07); }
.entity-nav-inner { max-width: 1240px; margin: auto; display: flex; flex-wrap: wrap; gap: 8px; }
.entity-link { display: inline-flex; align-items: center; min-height: 36px; padding: 0 13px; border: 1px solid transparent; border-radius: 10px; color: #aeb5cb; font-size: .82rem; font-weight: 700; text-decoration: none; transition: transform .16s ease-out,background .16s ease-out,color .16s ease-out; }
.entity-link:hover { transform: translateY(-1px); color: white; background: rgba(255,255,255,.06); }
.entity-link.active { color: white; background: var(--accent-soft); border-color: var(--accent-border); }
.entity-link:focus-visible,.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}.entity-nav{padding-top:10px;padding-bottom:10px}.entity-link{padding:0 10px;min-height:34px;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="entity-nav" aria-label="Portails par entité"><div class="entity-nav-inner" id="entity-nav"></div></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' }
};
var LEGACY_REDIRECTS = { 'portail.recette.santinova-soft.org': 'https://portail-santinova.recette.santinova-soft.org/', 'portail.santinova-soft.org': 'https://portail-santinova.santinova-soft.org/' };
var PORTAL_DEFINITIONS = [ { group:'ITINOVA',label:'Itinova',host:'portail-itinova' }, { group:'SOINS_ET_SANTE',label:'Soins et Santé',host:'portail-soins-et-sante' }, { group:'SANTINOVA',label:'Santinova',host:'portail-santinova' }, { group:'SANTELIS',label:'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 getPortalUrl(definition, env) { return 'https://' + definition.host + (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.recette.santinova-soft.org' : 'https://dashboard.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') return false; if (PORTAL.group === 'SANTINOVA') return app.category === 'SANTINOVA' || app.category === 'INFRA'; return app.category === PORTAL.group; }
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 renderNavigation() { document.getElementById('entity-nav').innerHTML = PORTAL_DEFINITIONS.map(function(definition) { var active = definition.group === PORTAL.group; return '<a class="entity-link' + (active ? ' active' : '') + '" href="' + getPortalUrl(definition, PORTAL.env) + '"' + (active ? ' aria-current="page"' : '') + '>' + definition.label + '</a>'; }).join(''); }
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() + '.'; renderNavigation(); }
function renderApps(apps) { var visibleApps = 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), LEGACY_REDIRECT = LEGACY_REDIRECTS[CURRENT_HOST], PORTAL = resolvePortalConfig(CURRENT_HOST);
if (LEGACY_REDIRECT) window.location.replace(LEGACY_REDIRECT);
else 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>