v3
This commit is contained in:
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"app": "/opt/alfred/app",
|
||||
"existing": [
|
||||
"static/js/dona-holographic-v1.js",
|
||||
"static/css/dona-holographic-v1.css",
|
||||
"templates/index.html"
|
||||
],
|
||||
"created": [
|
||||
"static/js/dona-markdown-renderer.js"
|
||||
]
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/* DONA_HOLOGRAPHIC_V1
|
||||
* Thème transversal non destructif. Les contrôleurs et APIs existants restent les mêmes.
|
||||
* Chargement APRÈS les feuilles Glass, y compris les correctifs précédents.
|
||||
*/
|
||||
:root { color-scheme:dark; --dh-white:#f1f4ff;--dh-muted:#a8b8d3;--dh-border:rgba(155,183,234,.23);--dh-panel:rgba(4,10,22,.81);--dh-blue:#5796ff;--dh-teal:#74e6d4;--dh-purple:#ab78ff;--dh-orange:#ffae73;--dh-shadow:0 24px 72px rgba(0,0,0,.38); }
|
||||
html {scrollbar-color:#596b8c #060a14;}
|
||||
body.dona-holo-v1 { color:var(--dh-white); background-color:#070913!important; background-image:
|
||||
radial-gradient(ellipse 35% 39% at 3% 70%,rgba(251,101,44,.5),transparent 95%),
|
||||
radial-gradient(ellipse 36% 52% at 96% 26%,rgba(18,125,236,.42),transparent 85%),
|
||||
radial-gradient(ellipse 42% 35% at 50% 0%,rgba(119,68,173,.31),transparent 90%),
|
||||
radial-gradient(ellipse 24% 32% at 82% 94%,rgba(243,97,42,.29),transparent 92%),
|
||||
linear-gradient(125deg,#070810,#0a1020 54%,#090914) !important;
|
||||
background-attachment:fixed!important; }
|
||||
body.dona-holo-v1::before{content:"";position:fixed;z-index:0;inset:-10%;pointer-events:none;
|
||||
background:radial-gradient(ellipse at 12% 18%,rgba(56,111,251,.17),transparent 33%),radial-gradient(ellipse at 78% 65%,rgba(227,100,165,.15),transparent 32%);
|
||||
filter:blur(68px);animation:dh-ambient 19s ease-in-out infinite alternate;}
|
||||
body.dona-holo-v1::after{content:"";position:fixed;inset:0;z-index:0;pointer-events:none;
|
||||
background:linear-gradient(90deg,rgba(1,5,13,.13),rgba(1,5,13,.45) 43%,rgba(1,5,13,.18));}
|
||||
@keyframes dh-ambient {0%{transform:translate(-1.5%,0)}100%{transform:translate(2%,1.4%)}}
|
||||
/* Écrans d'administration, qui utilisent des conteneurs et feuilles CSS différents. */
|
||||
body.dona-holo-v1 :is(.admin-layout,.id-shell,.id-wrap,.id-layout,.dv-main,.lv-layout,.lg-layout,.log-layout,.logs-layout){position:relative;z-index:1;}
|
||||
body.dona-holo-v1 .alfred-main-v1{z-index:1;}
|
||||
/* Priorité au panneau noir central de la maquette. Le décor reste diffus en périphérie. */
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-head {background:transparent!important;}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-logo {background:linear-gradient(145deg,#95c2ff,#5187ff 55%,#c58bff);background-clip:text;-webkit-background-clip:text;color:transparent;filter:drop-shadow(0 1px 18px #4486ff73);}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-brand small{color:#c4cde1;}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-round{background:rgba(5,9,22,.74)!important;border:1px solid rgba(155,174,224,.24)!important;box-shadow:inset 0 1px rgba(255,255,255,.04);}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-round[aria-pressed="true"]{background:rgba(108,28,58,.75)!important;border-color:rgba(255,116,151,.66)!important;}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-summary{gap:clamp(9px,1.05vw,18px);}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-stat-card{color:var(--dh-white)!important;
|
||||
background:linear-gradient(140deg,rgba(6,13,31,.79),rgba(8,16,34,.68))!important;
|
||||
border:1px solid rgba(126,157,227,.24)!important;border-radius:22px!important;
|
||||
box-shadow:0 12px 44px #01051255,inset 0 1px #ffffff0a!important;
|
||||
backdrop-filter:blur(20px) saturate(120%)!important;-webkit-backdrop-filter:blur(20px) saturate(120%)!important;
|
||||
transition:transform .2s,border-color .2s,box-shadow .2s;}
|
||||
body.dona-holo-v1 .alfred-main-v1 .av-stat-card:hover{transform:translateY(-2px);border-color:#8bb9ff83!important;box-shadow:0 14px 45px #0206128b,0 0 23px #336ce22c!important;background:rgba(10,21,42,.85)!important;}
|
||||
body.dona-holo-v1 .av-circle{background:#1e376566;color:#94c9ff;}
|
||||
body.dona-holo-v1 .av-circle.ai{background:#492b7a80;color:#d2b3ff;}
|
||||
body.dona-holo-v1 .av-weather{background:linear-gradient(140deg,rgba(12,28,44,.74),rgba(8,19,37,.8))!important;}
|
||||
body.dona-holo-v1 .av-shortcuts button{background:rgba(5,10,24,.82)!important;border:1px solid rgba(151,177,221,.27)!important;border-radius:21px!important;box-shadow:inset 0 1px rgba(255,255,255,.035);}
|
||||
body.dona-holo-v1 .av-shortcuts button:hover{background:#102344e6!important;border-color:#65a1ff82!important;}
|
||||
body.dona-holo-v1 .av-shortcuts .a-icon{color:#a9c6ff;}
|
||||
body.dona-holo-v1 .av-status-line{background:rgba(6,11,27,.65);border-color:rgba(157,186,229,.23);}
|
||||
body.dona-holo-v1 .av-wave-host canvas{filter:drop-shadow(0 0 9px rgba(91,137,255,.65))!important;}
|
||||
/* La réponse qui était une énorme div pleine page devient une scène avec cartes vivantes. */
|
||||
body.dona-holo-v1.alfred-glass-theme #stage {top:clamp(93px,11vh,128px)!important;right:clamp(12px,2vw,34px)!important;bottom:clamp(12px,2.3vh,30px)!important;left:clamp(12px,2vw,34px)!important;
|
||||
background:linear-gradient(146deg,rgba(2,5,13,.97),rgba(3,7,18,.96) 55%,rgba(6,10,26,.95))!important;
|
||||
border:1px solid rgba(154,180,231,.25)!important;border-radius:clamp(18px,2vw,31px)!important;
|
||||
padding:clamp(60px,8vh,100px) clamp(14px,3vw,46px) clamp(14px,3vh,28px)!important;
|
||||
box-shadow:0 36px 95px rgba(0,0,0,.55),inset 0 1px #d7dfff0d;
|
||||
display:flex!important;align-items:center!important;justify-content:center!important;overflow:auto!important;}
|
||||
body.dona-holo-v1.alfred-glass-theme #stage #content{width:min(1300px,100%)!important;max-width:none!important;max-height:none!important;min-height:0!important;overflow:visible!important;margin:auto;}
|
||||
body.dona-holo-v1.alfred-glass-theme #stage .av-reponse-actions {top:16px!important;right:20px!important;}
|
||||
body.dona-holo-v1.alfred-glass-theme #stage :is(#avBack.av-reponse-home,.av-reponse-close){background:rgba(17,27,51,.91)!important;border:1px solid #9bb9ec55!important;color:#f4f7ff!important;border-radius:15px!important;}
|
||||
body.dona-holo-v1.alfred-glass-theme #stage .av-reponse-close{border-radius:50%!important;}
|
||||
/* Réponse illustrée construite sur les données déjà présentes dans la réponse Hermes.
|
||||
Les éléments ne sont pas des chiffres simulés : un métrique n'apparaît que s'il est cité. */
|
||||
#content .dh-response{display:grid;grid-template-columns:minmax(250px,1.12fr) minmax(300px,.95fr);gap:clamp(12px,1.6vw,22px);align-items:stretch;min-width:0;}
|
||||
#content .dh-primary{position:relative;min-width:0;min-height:335px;display:flex;flex-direction:column;gap:20px;
|
||||
padding:clamp(22px,2.3vw,36px);border:1px solid rgba(96,142,255,.72);border-radius:24px;
|
||||
background:linear-gradient(135deg,rgba(13,30,62,.9),rgba(6,14,31,.89) 62%,rgba(15,36,42,.82));
|
||||
box-shadow:inset 0 1px #ffffff12,0 0 23px #3976fc2b,0 20px 35px #0004;}
|
||||
#content .dh-primary:before{content:"";position:absolute;inset:-1px;pointer-events:none;border-radius:24px;background:linear-gradient(120deg,#5b83ff33,transparent 44%,#72e8cd17);opacity:.7;}
|
||||
#content .dh-kicker{display:flex;align-items:center;gap:11px;font-size:11px!important;color:#a9bee6!important;letter-spacing:.16em;font-weight:700;text-transform:uppercase;}
|
||||
#content .dh-kicker:before{content:"";height:9px;width:9px;background:#61a3ff;border-radius:50%;box-shadow:0 0 12px #579cff;}
|
||||
#content .dh-headline{font-size:clamp(21px,2.1vw,33px)!important;letter-spacing:-.025em;font-weight:590;line-height:1.28;color:#f4f6ff;overflow-wrap:anywhere;}
|
||||
#content .dh-excerpt{color:#c8d2e6!important;font-size:clamp(13px,1.02vw,16px)!important;line-height:1.7;white-space:pre-wrap;overflow-wrap:anywhere;flex:1;}
|
||||
#content .dh-more{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:52px;padding:13px 16px;
|
||||
background:rgba(37,58,95,.44);border:1px solid #aac9ff42;border-radius:13px;color:#f0f5ff;font:600 13px/1.3 inherit;cursor:pointer;text-align:left;}
|
||||
#content .dh-more:hover{background:#294b748e;border-color:#95caff92;}
|
||||
#content .dh-facts{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));grid-auto-rows:minmax(145px,1fr);gap:14px;min-width:0;}
|
||||
#content .dh-fact{border:1px solid rgba(113,151,218,.35);border-radius:20px;padding:clamp(16px,1.7vw,25px);min-width:0;
|
||||
background:linear-gradient(135deg,rgba(17,34,63,.94),rgba(9,22,48,.85));box-shadow:inset 0 1px #ffffff0d;display:flex;flex-direction:column;gap:8px;}
|
||||
#content .dh-fact:nth-child(3n + 2){border-color:rgba(101,209,189,.29);background:linear-gradient(145deg,rgba(12,38,52,.94),rgba(13,26,45,.85));}
|
||||
#content .dh-fact:nth-child(3n + 3){border-color:rgba(239,150,94,.27);background:linear-gradient(135deg,rgba(45,29,40,.91),rgba(29,22,37,.86));}
|
||||
#content .dh-fact:nth-child(4n){border-color:rgba(167,124,239,.28);background:linear-gradient(135deg,rgba(31,26,60,.94),rgba(16,20,43,.86));}
|
||||
#content .dh-fact-label{font-size:clamp(11px,.85vw,14px)!important;line-height:1.35;color:#c1cee8;}
|
||||
#content .dh-fact-icon{font-size:21px;margin-bottom:6px;opacity:.9;}
|
||||
#content .dh-fact-value{font-size:clamp(19px,2.25vw,36px)!important;font-weight:570;line-height:1.15;color:#f9faff;letter-spacing:-.04em;overflow-wrap:anywhere;}
|
||||
#content .dh-fact-note{font-size:11px!important;color:#93a6c9;line-height:1.5;margin-top:auto;overflow-wrap:anywhere;}
|
||||
#content .dh-tags{display:flex;flex-wrap:wrap;gap:7px;margin-top:6px;}
|
||||
#content .dh-tag{padding:7px 11px;background:rgba(88,111,171,.2);border:1px solid rgba(143,166,218,.24);border-radius:999px;color:#d1daf6;font-size:12px;}
|
||||
#content .dh-full{grid-column:1/-1;border:1px solid rgba(145,174,227,.27);background:rgba(11,20,39,.82);border-radius:18px;padding:20px;}
|
||||
#content .dh-full[hidden]{display:none!important;}
|
||||
#content .dh-full h3{font-size:16px;margin:0 0 12px;color:#deecff;text-align:left;}
|
||||
#content .dh-full-text{white-space:pre-wrap;color:#ced8ea;line-height:1.65;font-size:14px;overflow-wrap:anywhere;}
|
||||
#content .dh-source{font-size:11px;color:#8b9fc1;margin-top:9px;}
|
||||
#content .dh-response:after{content:"";grid-column:1/-1;justify-self:center;width:min(490px,64%);height:3px;
|
||||
background:linear-gradient(90deg,transparent,#3e92fe 30%,#c876f8 55%,#f7a569 73%,transparent);border-radius:50%;box-shadow:0 0 18px 5px #529bff5b;margin:7px 0 0;}
|
||||
/* Les anciens types de cartes émis par les renderers restent pris en charge. */
|
||||
body.dona-holo-v1 #stage #content > .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:17px;text-align:left;}
|
||||
body.dona-holo-v1 #stage #content > .cards .card{padding:24px;border-radius:21px;background:linear-gradient(145deg,#13243ddf,#0a162ce5);border:1px solid #7c9cde57;text-align:left;}
|
||||
body.dona-holo-v1 #stage #content > img.hero {max-height:calc(100dvh - 260px);width:auto;max-width:100%;object-fit:contain;border-radius:16px;}
|
||||
/* Configuration et Surveillance */
|
||||
body.dona-holo-v1.alfred-admin-glass .admin-layout{z-index:1;}
|
||||
body.dona-holo-v1.alfred-admin-glass .ag-rail{background:rgba(3,7,20,.89)!important;border-color:var(--dh-border)!important;}
|
||||
body.dona-holo-v1.alfred-admin-glass .admin-header{background:transparent!important;}
|
||||
body.dona-holo-v1.alfred-admin-glass .brand-orb{background:linear-gradient(135deg,#438bff,#9368fa)!important;border-radius:18px;}
|
||||
body.dona-holo-v1.alfred-admin-glass :is(.panel,.metric-card,.auth-box,.ag-row,.mcp-item){
|
||||
background:linear-gradient(145deg,rgba(5,13,30,.91),rgba(9,18,36,.84))!important;
|
||||
border:1px solid rgba(133,164,229,.29)!important;border-radius:21px!important;box-shadow:var(--dh-shadow);backdrop-filter:blur(18px);}
|
||||
body.dona-holo-v1.alfred-admin-glass .page-top .eyebrow{color:#9cb9ff;}
|
||||
body.dona-holo-v1.alfred-admin-glass .button.primary{background:linear-gradient(125deg,#3063d0,#556dca)!important;border-color:#98baff88!important;}
|
||||
body.dona-holo-v1.alfred-admin-glass :is(.field input:not([type=range]):not([type=checkbox]),.field select,.ag-field input,.ag-field select){background:rgba(4,11,25,.87)!important;border-color:#799acc55!important;color:#f2f6ff!important;}
|
||||
body.dona-holo-v1.alfred-admin-glass .admin-header nav :is(a,button){background:rgba(6,13,31,.78)!important;border-color:#8aace950!important;}
|
||||
body.dona-holo-v1.alfred-admin-glass .admin-header nav .selected{border-color:#76a5fd!important;background:rgba(38,73,138,.92)!important;}
|
||||
/* Pages dont les classes proviennent d'extensions installées séparément. */
|
||||
body.dona-holo-v1 :is(.id-panel,.dv-panel,.lg-panel,.lv-panel,.log-panel,.logs-panel,.glass-panel,.id-editor,.dv-task,.dv-list,.dv-detail){background:linear-gradient(135deg,rgba(6,14,31,.92),rgba(12,20,42,.87))!important;
|
||||
border:1px solid rgba(127,158,221,.3)!important;border-radius:22px!important;box-shadow:0 15px 35px rgba(0,0,0,.22);backdrop-filter:blur(15px);}
|
||||
body.dona-holo-v1 :is(.dv-head,.id-header,.id-head,.lv-head,.lg-head,.logs-header,.log-header){position:relative;z-index:2;background:rgba(5,10,23,.66)!important;border-color:#819bd046!important;backdrop-filter:blur(15px);}
|
||||
body.dona-holo-v1 :is(.dv-head,.id-header,.id-head) :is(a,button){border-radius:12px;}
|
||||
body.dona-holo-v1 :is(.dv-main,.id-main,.lg-main,.lv-main,.logs-main,.log-main) h1{color:#f5f8ff;letter-spacing:-.035em;}
|
||||
body.dona-holo-v1 :is(input,select,textarea){accent-color:#7fa7ff;}
|
||||
body.dona-holo-v1 :is(.id-panel,.dv-panel,.lg-panel,.lv-panel,.log-panel,.logs-panel) :is(input:not([type=checkbox]):not([type=range]),textarea,select){background:rgba(5,11,27,.91)!important;color:#f2f6ff!important;border:1px solid #96b9ef55!important;border-radius:12px!important;}
|
||||
body.dona-holo-v1 :is(.dv-primary,.id-button.primary){background:linear-gradient(120deg,#376cdd,#6763d5)!important;color:white!important;}
|
||||
body.dona-holo-v1 :is(a,button,input,select,textarea):focus-visible{outline:2px solid #a9c5ff;outline-offset:3px;}
|
||||
/* Page Journaux : log monospace lisible, pas de glow derrière le contenu. */
|
||||
body.dona-holo-v1 :is(.lg-list,.logs-feed,.log-feed,.logs-table,.log-list,.lg-entries,pre.dv-diff){background:rgba(2,6,15,.79)!important;border:1px solid rgba(122,149,208,.25)!important;border-radius:14px!important;color:#d2e5ff;}
|
||||
/* Les modules secondaires restent visibles au-dessus des effets de fond. */
|
||||
body.dona-holo-v1 :is(dialog,.av-image-dialog,.camera-popup,#alfredTimersDock,#alfredTimerToast,.av-more-dialog,.audio-popover){z-index:100;}
|
||||
@media(max-width:1050px){#content .dh-response{grid-template-columns:1fr;}#content .dh-primary{min-height:220px}#content .dh-facts{grid-auto-rows:minmax(125px,auto);}body.dona-holo-v1.alfred-glass-theme #stage{align-items:flex-start!important;}}
|
||||
@media(max-width:700px){body.dona-holo-v1.alfred-glass-theme #stage{top:85px!important;left:8px!important;right:8px!important;bottom:8px!important;padding:80px 12px 18px!important;}#content .dh-response{display:block;}#content .dh-primary{padding:20px;margin-bottom:13px;}#content .dh-facts{grid-template-columns:repeat(2,minmax(0,1fr));gap:9px;}#content .dh-fact{padding:14px;min-height:130px;}#content .dh-full{margin-top:12px;}.alfred-glass-theme #stage .av-reponse-actions{top:12px!important;right:12px!important;}}
|
||||
@media(prefers-reduced-motion:reduce){body.dona-holo-v1::before{animation:none}body.dona-holo-v1 .av-stat-card{transition:none}}
|
||||
/* Finitions du panneau et affichage sans métriques */
|
||||
#content .dh-response{text-align:left;}
|
||||
#content .dh-response.dh-no-facts{grid-template-columns:minmax(0,900px);justify-content:center;}
|
||||
#content .dh-more{font-family:inherit;font-size:13px;font-weight:600;line-height:1.3;}
|
||||
body.dona-holo-v1, #content .dh-response{font-family:Inter,ui-sans-serif,system-ui,-apple-system,'Segoe UI',sans-serif;}
|
||||
/* Fenêtres secondaires de l'accueil (listes, ComfyUI, caméra) dans la même palette. */
|
||||
body.dona-holo-v1 :is(#alfredListsGlassDialog,.av-image-dialog,.av-comfy-dialog,.av-more-dialog,.av-popup,.camera-popup,.audio-popover){
|
||||
background:linear-gradient(145deg,rgba(4,10,24,.97),rgba(9,19,41,.96))!important;
|
||||
border:1px solid rgba(142,176,242,.38)!important;border-radius:22px!important;
|
||||
box-shadow:0 32px 90px #000a,0 0 26px #4f7fff18!important;color:#f4f7ff!important;
|
||||
backdrop-filter:blur(22px)!important;}
|
||||
body.dona-holo-v1 :is(.al-list-section,.al-item){background:rgba(14,25,48,.75)!important;border-color:#8aa9e34a!important;}
|
||||
body.dona-holo-v1 :is(.al-add-form button,.al-footer button){background:linear-gradient(120deg,#254a9f,#4f56bb)!important;border-color:#8baffb77!important;}
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/* DONA_HOLOGRAPHIC_V1
|
||||
* Cartes contextuelles issues du TEXTE réellement reçu d'Hermes.
|
||||
* Aucune donnée, état de serveur, distance ou durée n'est calculé/inventé.
|
||||
* Les autres vues (images, calendriers, cartes de renderer) restent inchangées.
|
||||
*/
|
||||
(() => {
|
||||
'use strict';
|
||||
const words = Object.freeze({zero:0,un:1,une:1,deux:2,trois:3,quatre:4,cinq:5,six:6,sept:7,huit:8,neuf:9,dix:10,onze:11,douze:12,treize:13,quatorze:14,quinze:15,seize:16,dixsept:17,dixhuit:18,dixneuf:19,vingt:20});
|
||||
function numberValue(s){
|
||||
if(!s)return null;
|
||||
const normalized=String(s).toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'').replace(/[\s-]/g,'');
|
||||
if(Object.prototype.hasOwnProperty.call(words,normalized))return String(words[normalized]);
|
||||
const str=String(s).trim();return /^\d{1,5}$/.test(str)?str:null;
|
||||
}
|
||||
const number='(\\d{1,5}|vingt|dix[- ]?neuf|dix[- ]?huit|dix[- ]?sept|seize|quinze|quatorze|treize|douze|onze|dix|neuf|huit|sept|six|cinq|quatre|trois|deux|une|un)';
|
||||
function getFacts(input) {
|
||||
const text=String(input||'').replace(/\u00a0/g,' ').slice(0,16000);
|
||||
const facts=[];const seen=new Set();
|
||||
function add(key,icon,label,value,note='Mentionné dans la réponse'){
|
||||
if(!value||seen.has(key)||facts.length>=4)return;
|
||||
facts.push({key,icon,label,value:String(value),note});seen.add(key);
|
||||
}
|
||||
let m;
|
||||
m=text.match(new RegExp('\\b'+number+'\\s+(?:machines?\\s+virtuelles?|VMs?\\b)','i'));
|
||||
if(m)add('vm','⬡','Machines virtuelles',numberValue(m[1]),'Chiffre annoncé par Hermes');
|
||||
m=text.match(new RegExp('\\b'+number+'\\s+(?:n[œo]uds?|nodes?)(?:\\s|\\.|,|$)','i'));
|
||||
if(m)add('nodes','▦','Nœuds mentionnés',numberValue(m[1]),'Chiffre annoncé par Hermes');
|
||||
m=text.match(/\b(\d{1,3})\s*\/\s*(\d{1,3})\s*(?:n[œo]uds|serveurs|nodes?)\b/i);
|
||||
if(m)add('online','●','Nœuds disponibles',m[1]+' / '+m[2],'Valeur citée par Hermes');
|
||||
const route=/\b(trajet|distance|route|entre\s+\S+\s+et\s+\S+|autoroute|en voiture)\b/i.test(text);
|
||||
const building=/\b(hauteur|haut(?:e|s)?|b[aâ]timent|immeuble|tour|monument|construction)\b/i.test(text);
|
||||
if(route){
|
||||
m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(kilom[eè]tres?|km)\b/i);
|
||||
if(m)add('distance','↗','Distance indiquée',m[1].replace(',', '.')+' km','Distance issue de la réponse');
|
||||
m=text.match(/\b(\d{1,3})\s*(?:à|a|–|-)\s*(\d{1,3})\s*(minutes?|min|heures?|h)\b/i);
|
||||
if(m)add('duration','◷','Durée annoncée',m[1]+' à '+m[2]+' '+m[3],'Estimation communiquée par Hermes');
|
||||
else {
|
||||
m=text.match(/\b(\d{1,3})\s*(minutes?|min|heures?|h)\b/i);
|
||||
if(m)add('duration','◷','Durée annoncée',m[1]+' '+m[2],'Estimation communiquée par Hermes');
|
||||
}
|
||||
}
|
||||
if(building&&!route){
|
||||
m=text.match(/\b(\d{1,5}(?:[,.]\d+)?)\s*(?:m[eè]tres?|m)\b/i);
|
||||
if(m)add('height','♜','Hauteur indiquée',m[1]+' m','Valeur issue de la réponse');
|
||||
}
|
||||
if(/\b(temp[eé]rature|m[eé]t[eé]o|degr[eé]s)\b/i.test(text)){
|
||||
m=text.match(/(-?\d{1,3}(?:[,.]\d+)?)\s*°\s*C\b/i);
|
||||
if(m)add('temp','☀','Température citée',m[1]+' °C','Valeur issue de la réponse');
|
||||
}
|
||||
// Ne jamais induire qu'un nœud est en ligne à partir du seul nom.
|
||||
if(facts.length<4){
|
||||
m=text.match(/[«"']\s*([\wÀ-ÿ][\wÀ-ÿ_.-]{1,39})\s*[»"'][^.!?\n]{0,90}(arr[eê]t[eé]e?|[eé]teint(?:e)?)(?=[\s,.!?]|$)/i);
|
||||
if(m)add('stopped','⏻','Équipement signalé arrêté',m[1],'État décrit dans la réponse');
|
||||
}
|
||||
if(facts.length===0){
|
||||
m=text.match(/\b(?:il est(?: actuellement)?|heure(?: locale)?\s*[::])\s*(\d{1,2}[:h]\d{2})\b/i);
|
||||
if(m)add('time','◷','Heure indiquée',m[1].replace('h',':'),'Issue de la réponse, non vérifiée ici');
|
||||
}
|
||||
const names=[];
|
||||
const re=/(?:n[œo]ud|node)\s*['"«]?([\wÀ-ÿ_.-]{2,35})['"»]?\s*:/gi;
|
||||
while((m=re.exec(text))&&names.length<10){if(!names.includes(m[1])) names.push(m[1]);}
|
||||
return {facts,names};
|
||||
}
|
||||
function summaryOf(text,title){
|
||||
const clean=String(text||'').trim();const t=String(title||'').trim();
|
||||
const first=clean.split(/(?<=[.!?])\s+|\n{2,}/)[0]?.trim()||clean;
|
||||
const headline=t && !/^(r[eé]ponse|hermes|texte)$/i.test(t)?t:first;
|
||||
const truncated=headline.length>170?headline.slice(0,167).replace(/\s+\S*$/,'')+'…':headline;
|
||||
let excerpt=clean;
|
||||
if(clean.startsWith(first)&&first.length>35)excerpt=clean.slice(first.length).replace(/^\s+/,'');
|
||||
if(!excerpt)excerpt=clean;
|
||||
if(excerpt.length>290)excerpt=excerpt.slice(0,287).replace(/\s+\S*$/,'')+'…';
|
||||
return {headline:truncated,excerpt};
|
||||
}
|
||||
if(typeof module==='object'&&module.exports){module.exports={getFacts,summaryOf,numberValue};}
|
||||
if(typeof document==='undefined')return;
|
||||
const body=document.body;
|
||||
if(!body||body.dataset.donaHoloV1==='active')return;
|
||||
body.dataset.donaHoloV1='active';body.classList.add('dona-holo-v1');
|
||||
const main=document.getElementById('alfredMainV1');
|
||||
const content=document.getElementById('content');
|
||||
if(!main||!content)return; // Pages secondaires : seulement le thème transversal.
|
||||
function el(tag,className,txt){const node=document.createElement(tag);if(className)node.className=className;if(txt!==undefined)node.textContent=String(txt);return node;}
|
||||
let busy=false;let original='';
|
||||
function renderCards(){
|
||||
if(busy || content.firstElementChild?.classList.contains('dh-response'))return;
|
||||
if(content.querySelector('.cards, img.hero, video, canvas, iframe'))return;
|
||||
// N'afficher les cartes que pour une réponse réellement ouverte par Alfred.
|
||||
if(!main.classList.contains('is-focused'))return;
|
||||
const children=[...content.children];
|
||||
if(!children.length)return;
|
||||
const text=children.filter(n=>n.matches('h1,h2,p,pre')).map(n=>n.textContent.trim()).filter(Boolean).join('\n\n').trim();
|
||||
if(!text || text.length<13)return;
|
||||
if(/^(bonjour\.|à votre écoute\.|en attente|chargement)/i.test(text))return;
|
||||
if(text===original && content.dataset.holoTransformed==='done')return;
|
||||
const title=content.querySelector('h1,h2')?.textContent||'';
|
||||
const raw=(title && text.startsWith(title+'\n\n'))?text.slice(title.length+2):text;
|
||||
const {facts,names}=getFacts(text);
|
||||
const summary=summaryOf(raw,title);
|
||||
const wrap=el('section','dh-response');
|
||||
wrap.setAttribute('aria-label','Synthèse contextuelle de la réponse Hermes');
|
||||
const primary=el('article','dh-primary');
|
||||
primary.append(el('div','dh-kicker','RÉPONSE · DONA'),el('div','dh-headline',summary.headline));
|
||||
if(summary.excerpt && summary.excerpt!==summary.headline)primary.append(el('div','dh-excerpt',summary.excerpt));
|
||||
else primary.append(el('div','dh-excerpt','Réponse disponible dans son intégralité.'));
|
||||
const more=el('button','dh-more');more.type='button';more.setAttribute('aria-expanded','false');more.append(el('span','', '▤ Afficher la réponse complète'),el('span','','›'));
|
||||
primary.append(more);wrap.append(primary);
|
||||
if(facts.length||names.length){
|
||||
const grid=el('div','dh-facts');
|
||||
facts.forEach(f=>{
|
||||
const c=el('article','dh-fact');
|
||||
c.append(el('span','dh-fact-icon',f.icon),el('div','dh-fact-label',f.label),el('strong','dh-fact-value',f.value),el('div','dh-fact-note',f.note));
|
||||
grid.append(c);
|
||||
});
|
||||
if(names.length && grid.children.length<4){
|
||||
const c=el('article','dh-fact');c.append(el('span','dh-fact-icon','⬡'),el('div','dh-fact-label','Nœuds cités'));
|
||||
const tags=el('div','dh-tags');names.forEach(n=>tags.append(el('span','dh-tag',n)));c.append(tags,el('div','dh-fact-note','Noms présents dans la réponse'));grid.append(c);
|
||||
}
|
||||
wrap.append(grid);
|
||||
} else {
|
||||
wrap.classList.add('dh-no-facts');
|
||||
}
|
||||
const full=el('section','dh-full');full.hidden=true;full.id='dhFullReply';
|
||||
full.append(el('h3','', 'Réponse complète d’Hermes'),el('div','dh-full-text',text),el('p','dh-source','Affichage fidèle au texte reçu. Les faits n’ont pas été vérifiés séparément.'));
|
||||
more.setAttribute('aria-controls','dhFullReply');
|
||||
more.addEventListener('click',()=>{
|
||||
const opened=full.hidden;full.hidden=!opened;more.setAttribute('aria-expanded',String(opened));
|
||||
more.firstElementChild.textContent=opened?'▤ Masquer la réponse complète':'▤ Afficher la réponse complète';
|
||||
more.lastElementChild.textContent=opened?'⌃':'›';
|
||||
});
|
||||
wrap.append(full);
|
||||
busy=true;content.replaceChildren(wrap);content.dataset.holoTransformed='done';original=text;busy=false;
|
||||
}
|
||||
const obs=new MutationObserver(()=>{if(!busy)renderCards();});
|
||||
obs.observe(content,{childList:true});
|
||||
// Accès à l'écran de réponse après mise à jour de la classe focus, sans boucle.
|
||||
main.addEventListener('click',()=>setTimeout(renderCards,0));
|
||||
// Le renderer/socket réactualise #content : la mutation constitue l'événement canonique.
|
||||
setTimeout(renderCards,100);
|
||||
})();
|
||||
@@ -0,0 +1,224 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="theme-color" content="#202326">
|
||||
<title>{{ name }}</title>
|
||||
<link rel="stylesheet" href="/static/css/alfred.css">
|
||||
<link rel="stylesheet" href="/static/css/alfred-controls.css">
|
||||
<link rel="stylesheet" href="/static/css/alfred-views.css">
|
||||
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
||||
<link rel="stylesheet" href="/static/css/alfred-camera-popup.css">
|
||||
<link rel="stylesheet" href="/static/css/alfred-features.css?v=45">
|
||||
<link rel="stylesheet" href="/static/css/alfred-timers-context.css">
|
||||
<!-- ALFRED_MAIN_VISUAL_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-main-v1.css?v=1">
|
||||
<!-- ALFRED_LISTES_GLASS_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-listes-glass.css?v=1">
|
||||
<!-- ALFRED_GLASS_V2 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-main-v2.css?v=2">
|
||||
<!-- ALFRED_STT_INLINE_GLASS_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-stt-inline-v1.css?v=1">
|
||||
<!-- ALFRED_REPONSE_CLOSE_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-reponse-close-v1.css?v=1">
|
||||
<!-- ALFRED_HERMES_NEW_SESSION_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/alfred-hermes-new-session.css?v=1">
|
||||
<!-- DONA_HOLO_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/dona-holographic-v1.css?v=1">
|
||||
<!-- DONA_FAMILY_UI_V1 -->
|
||||
<link rel="stylesheet" href="/static/css/dona-accueil-ecrit-v1.css?v=1">
|
||||
<link rel="stylesheet" href="/static/css/dona-v6.css?v=6">
|
||||
</head>
|
||||
<body>
|
||||
<main class="alfred-shell">
|
||||
<header class="topbar">
|
||||
<!-- Le statut remplace l'ancien titre « Alfred ». -->
|
||||
<div class="status" role="status" aria-live="polite">
|
||||
<span id="statusDot" class="status-dot" data-mode="idle" aria-hidden="true"></span>
|
||||
<span id="mode" data-mode="idle">En attente</span>
|
||||
</div>
|
||||
|
||||
<div class="header-actions" aria-label="Navigation et commandes audio">
|
||||
<!-- ALFRED_ADMIN_NAV_V1 : pages d’administration séparées -->
|
||||
<a class="icon-button" style="text-decoration:none" href="/config" title="Configuration complète" aria-label="Configuration complète">⚙</a>
|
||||
<a class="icon-button" style="text-decoration:none" href="/surveillance" title="Surveillance des services" aria-label="Surveillance des services">▤</a>
|
||||
<button id="viewHome" class="icon-button" type="button" title="Accueil" aria-label="Accueil">⌂</button>
|
||||
<button id="viewCalendar" class="icon-button" type="button" title="Agenda" aria-label="Agenda">▦</button>
|
||||
<button id="viewWeather" class="icon-button" type="button" title="Météo Home Assistant" aria-label="Météo Home Assistant">☀</button>
|
||||
<button id="viewTemperatures" class="icon-button" type="button" title="Températures" aria-label="Températures">°</button>
|
||||
<button id="viewCameras" class="icon-button" type="button" title="Caméras Home Assistant" aria-label="Caméras Home Assistant">▣</button>
|
||||
|
||||
<!-- ALFRED_CAMERA_POPUP_V1 -->
|
||||
<button id="cameraPopupButton" class="icon-button" type="button"
|
||||
title="Afficher la caméra" aria-label="Afficher la caméra"
|
||||
aria-haspopup="dialog" aria-controls="cameraPopup" aria-expanded="false">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7"
|
||||
stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="2.5" y="5" width="14" height="14" rx="3"/>
|
||||
<path d="m16.5 10 5-3v10l-5-3"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="audioSettingsButton" class="icon-button" type="button"
|
||||
aria-label="Réglages audio et caméra" aria-haspopup="dialog" aria-expanded="false"
|
||||
aria-controls="audioSettingsPanel" title="Réglages audio et caméra">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M4 7h16M4 17h16"/><circle cx="9" cy="7" r="2.5" fill="#272b30"/><circle cx="16" cy="17" r="2.5" fill="#272b30"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="micMute" class="icon-button" type="button"
|
||||
aria-label="Couper le microphone" aria-pressed="false" title="Couper le microphone">
|
||||
<svg class="mic-on-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<rect x="9" y="2.5" width="6" height="12" rx="3"/>
|
||||
<path d="M5.5 10.5v1.5a6.5 6.5 0 0 0 13 0v-1.5M12 18.5v3M8 21.5h8"/>
|
||||
</svg>
|
||||
<svg class="mic-off-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M9.2 5.3V5.5a3 3 0 0 1 5.5-1.4M15 10.6v.9c0 .5-.1 1-.3 1.5M9 9.5v2A3 3 0 0 0 13.6 14"/>
|
||||
<path d="M5.5 10.5V12a6.5 6.5 0 0 0 11.5 4.1M12 18.5v3M8 21.5h8M3 3l18 18"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="speakerMuteQuick" class="icon-button" type="button"
|
||||
aria-label="Couper le son" aria-pressed="false" title="Couper le son">
|
||||
<svg class="speaker-on-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m11 5-5 4H3v6h3l5 4V5Z"/><path d="M15 9a4 4 0 0 1 0 6M18 6a8 8 0 0 1 0 12"/>
|
||||
</svg>
|
||||
<svg class="speaker-off-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="m11 5-5 4H3v6h3l5 4V5Z"/><path d="m16 9 5 6m0-6-5 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Pop-up : commandes connectées à /api/audio/settings -->
|
||||
<div id="audioSettingsPanel" class="audio-popover" role="dialog"
|
||||
aria-label="Réglages audio et caméra" aria-modal="false" hidden>
|
||||
<div class="popover-heading">
|
||||
<strong>Réglages</strong>
|
||||
<button id="closeAudioSettings" class="icon-button mini" type="button" aria-label="Fermer les réglages">×</button>
|
||||
</div>
|
||||
<div class="settings-tabs" role="tablist" aria-label="Catégories de réglages">
|
||||
<button id="audioTab" class="settings-tab" type="button" role="tab" aria-selected="true" aria-controls="audioSettingsSection">Audio</button>
|
||||
<button id="cameraTab" class="settings-tab" type="button" role="tab" aria-selected="false" aria-controls="cameraSettingsSection">Caméra</button>
|
||||
</div>
|
||||
<div id="audioSettingsSection" role="tabpanel" aria-labelledby="audioTab">
|
||||
<div class="setting-row">
|
||||
<label for="volumeSlider">Volume du haut-parleur</label>
|
||||
<output id="volumeValue" for="volumeSlider">70 %</output>
|
||||
</div>
|
||||
<input id="volumeSlider" type="range" min="0" max="100" value="70" step="1">
|
||||
<div class="setting-row">
|
||||
<label for="micGainSlider">Sensibilité du microphone</label>
|
||||
<output id="micGainValue" for="micGainSlider">—</output>
|
||||
</div>
|
||||
<input id="micGainSlider" type="range" min="0" max="100" value="50" step="1">
|
||||
<p id="audioSettingsMessage" class="settings-message" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
<div id="cameraSettingsSection" role="tabpanel" aria-labelledby="cameraTab" hidden>
|
||||
<p class="camera-settings-note">Webcam locale · réglages matériels V4L2.<br>Tu peux voir le résultat via le bouton Caméra en haut de l’écran.</p>
|
||||
<div id="cameraSettingsItems" class="camera-settings-items" aria-live="off"></div>
|
||||
<div class="camera-settings-actions">
|
||||
<button id="cameraSettingsReload" class="camera-settings-action" type="button">Actualiser</button>
|
||||
<button id="cameraSettingsReset" class="camera-settings-action reset" type="button">Valeurs d’usine</button>
|
||||
</div>
|
||||
<p id="cameraSettingsMessage" class="settings-message" role="status" aria-live="polite"></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="cameraPopup" class="camera-popup-overlay" role="dialog"
|
||||
aria-label="Prévisualisation caméra" aria-modal="true" hidden>
|
||||
<div class="camera-popup-frame">
|
||||
<div class="camera-popup-titlebar">
|
||||
<strong class="camera-popup-title">Caméra en direct</strong>
|
||||
<button id="cameraPopupClose" class="camera-popup-close" type="button"
|
||||
aria-label="Fermer la caméra" title="Fermer">×</button>
|
||||
</div>
|
||||
<aside class="camera-panel camera-popup-body" aria-label="Prévisualisation caméra">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-title"><span class="panel-indicator"></span> CAMÉRA</span>
|
||||
<button id="cameraToggle" class="panel-action" type="button" aria-pressed="false">Activer</button>
|
||||
</div>
|
||||
<div class="camera-viewport">
|
||||
<img id="cameraFeed" alt="Vidéo en direct de la caméra d'Alfred" hidden>
|
||||
<div id="cameraPlaceholder" class="camera-placeholder">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.4" aria-hidden="true">
|
||||
<rect x="2.5" y="5" width="14" height="14" rx="3"/><path d="m16.5 10 5-3v10l-5-3"/>
|
||||
</svg>
|
||||
<span id="cameraMessage">Caméra désactivée</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<p class="camera-popup-footnote">Le flux est interrompu à la fermeture pour laisser la webcam disponible aux autres fonctions d’Alfred.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="stage" aria-label="Réponse d'Alfred">
|
||||
<div id="content"><h1>Bonjour.</h1><p>À votre écoute.</p></div>
|
||||
</section>
|
||||
|
||||
<section id="wave" aria-hidden="true"><canvas id="waveCanvas"></canvas></section>
|
||||
|
||||
|
||||
|
||||
<!-- Affiche les résultats partiels ET définitifs de la reconnaissance Vosk. -->
|
||||
<aside class="transcript-panel" aria-label="Transcription vocale">
|
||||
<div class="panel-heading">
|
||||
<span class="panel-title"><span class="panel-indicator stt-indicator"></span> RECONNAISSANCE VOCALE · VOSK</span>
|
||||
<span id="transcriptState" class="transcript-state">En attente</span>
|
||||
</div>
|
||||
<p id="transcriptText" class="transcript-text" aria-live="polite">La dernière phrase reconnue s'affichera ici.</p>
|
||||
</aside>
|
||||
</main>
|
||||
|
||||
<script src="/static/js/alfred.js"></script>
|
||||
<script src="/static/js/alfred-views.js"></script>
|
||||
<script src="/static/js/alfred-controls.js"></script>
|
||||
<script src="/static/js/alfred-camera.js"></script>
|
||||
<script src="/static/js/alfred-camera-settings.js"></script>
|
||||
<script src="/static/js/alfred-camera-popup.js"></script>
|
||||
<script defer src="/static/js/alfred-features.js?v=45"></script>
|
||||
|
||||
<!-- ALFRED_TIMERS_CONTEXT_V1 : chronos/minuteurs dans le coin inférieur gauche -->
|
||||
<aside id="alfredTimersDock" role="region" aria-label="Minuteurs et chronomètres" hidden>
|
||||
<div class="alfred-time-head">⏱ MINUTEURS ET CHRONOMÈTRES</div>
|
||||
<div id="alfredTimersList"></div>
|
||||
</aside>
|
||||
<div id="alfredTimerToast" role="alert" aria-live="assertive" hidden></div>
|
||||
|
||||
<div id="alfredContextPopup" role="dialog" aria-label="Illustration de la réponse"
|
||||
aria-modal="true" hidden>
|
||||
<article class="alfred-context-card">
|
||||
<button id="alfredContextClose" class="alfred-context-close" type="button"
|
||||
aria-label="Fermer l'illustration" title="Fermer">×</button>
|
||||
<img id="alfredContextImage" alt="Illustration contextuelle de la réponse" />
|
||||
<div class="alfred-context-details">
|
||||
<h2 id="alfredContextTitle"></h2>
|
||||
<p id="alfredContextText"></p>
|
||||
<a id="alfredContextSource" href="#" target="_blank" rel="noopener noreferrer">Source Wikipédia</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
<script src="/static/js/alfred-timers-context.js"></script>
|
||||
<!-- ALFRED_HIDE_HERMES_ACTIVITY_V2 -->
|
||||
<script src="/static/js/alfred-hide-hermes-activity.js?v=2" defer></script>
|
||||
<script src="/static/js/alfred-main-v1.js?v=1"></script>
|
||||
<!-- ALFRED_ADMIN_GLASS_MAISON_V1 -->
|
||||
<script src="/static/js/alfred-maison-card.js?v=1"></script>
|
||||
<script src="/static/js/alfred-listes-glass.js?v=1"></script>
|
||||
<script src="/static/js/alfred-main-v2.js?v=2"></script>
|
||||
<!-- ALFRED_IDENTITY_GLASS_V1 -->
|
||||
<script src="/static/js/alfred-name-dynamic.js?v=1" defer></script>
|
||||
<script src="/static/js/alfred-stt-inline-v1.js?v=1"></script>
|
||||
<script src="/static/js/alfred-reponse-close-v1.js?v=1"></script>
|
||||
<!-- ALFRED_DEVELOPER_V1 -->
|
||||
<script src="/static/js/alfred-maintenance-link.js?v=1" defer></script>
|
||||
<script src="/static/js/alfred-hermes-new-session.js?v=1"></script>
|
||||
<script defer src="/static/js/dona-holographic-v1.js?v=1"></script>
|
||||
<!-- DONA_REPLY_AUTO_CLOSE_V1 -->
|
||||
<script defer src="/static/js/alfred-reply-auto-close.js?v=1"></script>
|
||||
<script src="/static/js/dona-accueil-ecrit-v1.js?v=6"></script>
|
||||
<script defer src="/static/js/dona-spotify.js?v=6"></script>
|
||||
<!-- dona-ui-fix start -->
|
||||
<script defer src="{{ url_for('static', filename='js/dona-ui-fix.js') }}?v=6"></script>
|
||||
<!-- dona-ui-fix end -->
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user