v0.5.1 : accès distant ajean.link depuis l'UI + app Windows sans console

- Panneau « Accès distant » dans l'UI web : abonnement + liaison ajean.link
  en un clic (popup connect.html → clé remise à l'agent local), sans terminal.
  Endpoints locaux /api/link/status|connect|disconnect|paircode (web_link_api.go).
- Accès OpenAI public : refuse d'être activé sans accès distant, avec message.
- Windows : binaire compilé en sous-système GUI (-H=windowsgui) → plus aucune
  fenêtre de console au double-clic ; rattachement à la console parente en CLI
  (sys_console_windows.go). Suppression du hack relaunchDetachedApp.
- Windows : masquage (CREATE_NO_WINDOW) de TOUS les sous-process — git/vswhere
  du panneau MOTEUR (flash à chaque refresh), llama-server, MCP stdio, winget…
- Vouvoiement du nouveau parcours.
This commit is contained in:
nathaninline committed 2026-07-24 16:39:09 +02:00
1 parent 8e57685b19
commit d8b08dbb89
22 files changed
+444 -103

No files matched your search

+24
View File
@@ -124,6 +124,30 @@
<button onclick="apiKeyAction('clear')">retirer</button>
</div>
</details>
<details id="remote-details"><summary>Accès distant <span id="remote-badge" style="margin-left:auto;margin-right:12px;font-size:10px"></span></summary>
<div class="subhead">ajean.link<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Accédez à ce serveur Jean depuis <b>n'importe où</b>, sans ouvrir de port, à travers le relais <b>ajean.link</b>. Le trafic est chiffré de <b>bout en bout</b> : le relais ne voit rien. Abonnement 4,80 €/mois.</span></span></div>
<!-- Non connecté : un seul bouton, tout se passe dans la popup (compte + paiement). -->
<div id="remote-off">
<p class="muted" style="font-size:12px;line-height:1.5;margin:6px 0 10px">Ouvrez l'accès distant à ce serveur en quelques clics — compte, abonnement et connexion, sans terminal.</p>
<button onclick="remoteConnect()" style="border-color:var(--accent);color:var(--accent)">🔗 Connecter à ajean.link</button>
</div>
<!-- Connecté : URL publique + appairage + déconnexion. -->
<div id="remote-on" style="display:none">
<div class="muted" style="font-size:11px;margin-bottom:2px">🌍 votre adresse d'accès distant</div>
<div class="row">
<input id="remote-url" readonly style="flex:1;min-width:0;background:var(--panel);color:var(--text);border:1px solid var(--border);border-radius:6px;padding:6px;font:inherit;font-size:12px">
<button onclick="copyText(document.getElementById('remote-url').value,'adresse copiée')" title="copier">⧉</button>
<button onclick="window.open(document.getElementById('remote-url').value,'_blank')" title="ouvrir">↗</button>
</div>
<div id="remote-status" class="muted" style="font-size:11px;margin-top:6px"></div>
<div class="subhead" style="margin-top:12px">Première connexion<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">À votre première ouverture du portail distant, confirmez l'<b>empreinte</b> et saisissez le <b>code d'appairage</b> : cela verrouille la boîte noire sur CE serveur (anti-interception). Une seule fois par appareil.</span></span></div>
<button onclick="remotePairCode()" style="margin-top:4px">🔑 Afficher l'empreinte + le code d'appairage</button>
<div id="remote-pair" class="muted" style="font-size:12px;margin-top:8px;display:none;line-height:1.7"></div>
<div class="row" style="margin-top:12px">
<button onclick="remoteDisconnect()" style="border-color:var(--err);color:var(--err)">déconnecter l'accès distant</button>
</div>
</div>
</details>
<details id="lc-details"><summary>MOTEUR</summary>
<div id="lc-status" class="lc-head muted">…</div>
<div class="lc-modes">
+14 -2
View File
@@ -195,7 +195,19 @@ async function apiKeyAction(action){
renderApiKey(await jpost('/api/apikey', {action}));
}
async function toggleOAIPublic(){
const on = document.getElementById('oai-public-toggle').checked;
const cb = document.getElementById('oai-public-toggle');
const on = cb.checked;
// L'accès OpenAI public passe par ajean.link (<machine>.oai.ajean.link) : il exige
// que l'accès distant soit activé sur ce serveur. Sinon, on annule et on explique.
if(on){
let linked = false;
try{ const s = await jget('/api/link/status'); linked = !!(s && s.linked); }catch(e){}
if(!linked){
cb.checked = false;
await askAlert('Vous devez activer l\'accès distant (ajean.link) pour bénéficier de cette fonctionnalité. Ouvrez le panneau « Accès distant » pour connecter ce serveur.', {title:'Accès distant requis'});
return;
}
}
await jpost('/api/oai/public', {enabled:on});
toast(on ? 'accès public activé' : 'accès public coupé');
loadApiKey();
@@ -216,5 +228,5 @@ async function saveCrawlUrl(){
const url=document.getElementById('crawl-url').value.trim();
renderInternet(await jpost('/api/internet',{url}));
}
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp()]); }
async function loadAll(){ await Promise.all([loadStatus(),loadVram(),loadRam(),loadCfg(),loadPresets(),loadAgent(),loadInternet(),loadMCP(),loadApiKey(),loadPrefs(),loadLlamacpp(),loadRemote()]); }
async function act(a){ toast(a+'…'); await jpost('/api/'+a); setTimeout(loadAll,1500); }
+77
View File
@@ -0,0 +1,77 @@
// Accès distant (ajean.link) — piloté depuis l'UI, sans terminal.
// « Connecter » ouvre une popup app.ajean.link/connect.html qui gère compte +
// abonnement, puis renvoie une clé de liaison par postMessage. On la POSTe à
// l'agent local (/api/link/connect) qui fait le `jean link` (token + service).
const AJEAN_APP_ORIGIN = 'https://app.ajean.link';
function renderRemote(d){
const off = document.getElementById('remote-off'), on = document.getElementById('remote-on');
const badge = document.getElementById('remote-badge');
if(!d || !d.linked){
off.style.display=''; on.style.display='none';
if(badge){ badge.textContent=''; }
return;
}
off.style.display='none'; on.style.display='';
document.getElementById('remote-url').value = d.machineURL || '';
const st = document.getElementById('remote-status');
if(d.active){ st.textContent='● en ligne — accessible à distance'; st.style.color='var(--accent)'; }
else { st.textContent='○ service arrêté (l\'accès distant ne répondra pas)'; st.style.color='var(--warn)'; }
if(badge){ badge.textContent = d.active ? '● connecté' : '○ arrêté'; badge.style.color = d.active?'var(--accent)':'var(--warn)'; }
}
async function loadRemote(){
// Le panneau ne vaut qu'en LOCAL : il pilote /api/link/* que le tunnel bloque
// (boîte noire). Sur le portail distant (app.ajean.link/server.html), on le cache.
const det = document.getElementById('remote-details');
if(location.hostname === 'app.ajean.link'){ if(det) det.style.display='none'; return; }
try{ renderRemote(await jget('/api/link/status')); }catch(e){}
}
// Ouvre la popup de connexion et attend la clé renvoyée par postMessage.
function remoteConnect(){
const params = new URLSearchParams({ origin: window.location.origin, host: window.location.hostname || 'ce serveur' });
const url = AJEAN_APP_ORIGIN + '/connect.html?' + params.toString();
const pop = window.open(url, 'ajean-connect', 'width=440,height=640');
if(!pop){ toast('autorisez les popups pour connecter ajean.link'); return; }
async function onMsg(ev){
// Anti-usurpation : n'accepte QUE des messages du portail ajean.link.
if(ev.origin !== AJEAN_APP_ORIGIN) return;
const d = ev.data || {};
if(d.type !== 'ajean-link' || !d.token) return;
window.removeEventListener('message', onMsg);
try{
const r = await jpost('/api/link/connect', { token: d.token });
if(r && r.linked){
toast('✓ accès distant connecté');
if(r.serviceErr){ toast('token enregistré (service : '+r.serviceErr+')'); }
loadRemote();
} else {
toast((r && r.error) || 'échec de la connexion');
}
}catch(e){ toast('erreur : '+e); }
}
window.addEventListener('message', onMsg);
}
async function remoteDisconnect(){
const ok = await askConfirm('Couper l\'accès distant et oublier la clé de liaison de ce serveur ?', {title:'Accès distant', okLabel:'Déconnecter'});
if(!ok) return;
try{ await jpost('/api/link/disconnect', {}); toast('accès distant coupé'); loadRemote(); }
catch(e){ toast('erreur : '+e); }
}
async function remotePairCode(){
const box = document.getElementById('remote-pair');
box.style.display=''; box.textContent='génération du code…';
try{
const r = await jpost('/api/link/paircode', {});
if(r && r.code){
box.innerHTML = 'Empreinte : <b>'+(r.fingerprint||'—')+'</b><br>Code d\'appairage (valable 10 min, usage unique) : <b>'+r.code+'</b>';
} else {
box.textContent = (r && r.error) || 'code indisponible';
}
}catch(e){ box.textContent='erreur : '+e; }
}