830cb5d679
Stack ttyd + tmux + Caddy, reproduction d'une installation en production. Inclut le frontend personnalise et les correctifs deja valides (FitAddon, refit apres changement de police, double collage Ctrl+V, OSC 52, window.open). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
950 lines
38 KiB
Markdown
950 lines
38 KiB
Markdown
# Mode operatoire : acces web a une session Claude Code persistante
|
||
|
||
Document destine a etre donne tel quel a une instance Claude Code disposant d'un acces root
|
||
sur l'hyperviseur / le serveur cible. Reproduit une stack deja en production.
|
||
|
||
Fichier joint indispensable : `ttyd-custom.html` (frontend personnalise, etape 6).
|
||
Si tu ne l'as pas, la section 6.3 contient son contenu integral.
|
||
|
||
---
|
||
|
||
## 0. Resultat vise et architecture
|
||
|
||
Depuis n'importe quel navigateur, on ouvre `https://claude.mondomaine.fr`, on saisit un
|
||
identifiant, et on retombe sur **la meme session Claude Code**, exactement la ou elle en
|
||
etait, y compris apres fermeture du navigateur, redemarrage du PC ou changement d'appareil.
|
||
|
||
```
|
||
navigateur
|
||
-> https://claude.mondomaine.fr (443, TLS Let's Encrypt)
|
||
-> reverse proxy Caddy (basic_auth + websocket)
|
||
-> conteneur/VM claude-code : 7681 (jamais expose publiquement)
|
||
-> ttyd 1.7.7 (frontend xterm.js personnalise)
|
||
-> tmux attach-session -t claude (persistance de la session)
|
||
-> claude (Claude Code CLI)
|
||
```
|
||
|
||
Points de conception qui comptent :
|
||
|
||
- La persistance vient de **tmux**, pas de ttyd. ttyd n'est qu'un pont websocket vers un PTY.
|
||
- tmux tourne dans un service systemd **distinct** de ttyd. Redemarrer ttyd ne tue donc jamais
|
||
la session. C'est ce qui rend les mises a jour du frontend sans risque.
|
||
- L'authentification est faite par le **reverse proxy**, pas par ttyd. Le port 7681 doit rester
|
||
inaccessible depuis l'exterieur.
|
||
|
||
Reference materielle de l'installation existante (largement suffisant) :
|
||
LXC Proxmox non privilegie, Ubuntu 24.04, 4 vCPU, 4 Go RAM, disque 20 Go.
|
||
|
||
---
|
||
|
||
## 1. Prerequis
|
||
|
||
| Element | Detail |
|
||
|---|---|
|
||
| Un domaine | ex. `mondomaine.fr`, avec acces a la zone DNS |
|
||
| Une IP publique | fixe, ou dynamique + client DDNS |
|
||
| Acces au routeur/box | pour rediriger les ports 80 et 443 |
|
||
| Un serveur Linux | conteneur LXC, VM ou machine physique, x86_64 |
|
||
| Un compte Claude | abonnement Claude ou cle API Anthropic |
|
||
|
||
Choisir des maintenant :
|
||
- le sous-domaine : `claude.mondomaine.fr`
|
||
- l'IP interne du serveur Claude : ex. `192.168.1.123`
|
||
- l'IP interne du reverse proxy : ex. `192.168.1.2`
|
||
|
||
Le reverse proxy et le serveur Claude peuvent etre la meme machine. Le document les separe
|
||
car c'est plus propre, mais l'etape 7 indique comment tout regrouper.
|
||
|
||
---
|
||
|
||
## 2. Etape 1 : creer le conteneur
|
||
|
||
Sous Proxmox (adapter les identifiants et le pont reseau) :
|
||
|
||
```bash
|
||
pct create 123 local:vztmpl/ubuntu-24.04-standard_24.04-2_amd64.tar.zst \
|
||
--hostname claude-code \
|
||
--cores 4 --memory 4096 --swap 512 \
|
||
--rootfs local-lvm:20 \
|
||
--net0 name=eth0,bridge=vmbr0,ip=192.168.1.123/24,gw=192.168.1.1 \
|
||
--unprivileged 1 --features nesting=1 \
|
||
--onboot 1 --start 1
|
||
```
|
||
|
||
`nesting=1` n'est necessaire que si tu comptes faire tourner Docker ou des outils qui montent
|
||
des systemes de fichiers dans le conteneur. Sans cela, ce n'est pas requis.
|
||
|
||
Sur une VM ou une machine physique : installer Ubuntu 24.04, IP fixe, c'est tout.
|
||
|
||
Toute la suite s'execute **dans** ce serveur, en root.
|
||
|
||
---
|
||
|
||
## 3. Etape 2 : base systeme
|
||
|
||
```bash
|
||
apt update && apt install -y \
|
||
curl git tmux ca-certificates locales build-essential \
|
||
python3 jq unzip
|
||
|
||
# Locale UTF-8 : indispensable, sinon les accents et les caracteres de dessin
|
||
# de l'interface Claude Code s'affichent en carres.
|
||
sed -i 's/^# *fr_FR.UTF-8/fr_FR.UTF-8/' /etc/locale.gen
|
||
locale-gen
|
||
update-locale LANG=fr_FR.UTF-8
|
||
|
||
# Node.js 22 LTS
|
||
curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
|
||
apt install -y nodejs
|
||
|
||
# Claude Code
|
||
npm install -g @anthropic-ai/claude-code
|
||
claude --version
|
||
```
|
||
|
||
Configuration tmux minimale mais **obligatoire** pour le confort de l'interface web :
|
||
|
||
```bash
|
||
cat > /root/.tmux.conf <<'EOF'
|
||
set -g mouse on
|
||
set -g set-clipboard on
|
||
EOF
|
||
```
|
||
|
||
- `mouse on` : molette = defilement de l'historique, selection a la souris.
|
||
- `set-clipboard on` : tmux emet des sequences OSC 52, que le frontend convertit en
|
||
ecriture dans le presse-papier du navigateur. Sans cela, la copie depuis le terminal
|
||
distant ne remonte pas jusqu'a la machine cliente.
|
||
|
||
Authentification Claude : lancer `claude` une premiere fois en SSH et suivre le flux de
|
||
connexion. Les identifiants sont stockes dans `/root/.claude/.credentials.json`.
|
||
|
||
Notification de fin de commande (facultatif, utilise par l'etape 6) :
|
||
|
||
```bash
|
||
cat >> /root/.bashrc <<'EOF'
|
||
|
||
# Notification terminal : emet OSC 777 a chaque prompt (capte par le JS de ttyd)
|
||
__ttyd_prompt_cmd() {
|
||
local code=$?
|
||
printf '\033]777;%d\007' "$code"
|
||
}
|
||
PROMPT_COMMAND="${PROMPT_COMMAND:+$PROMPT_COMMAND; }__ttyd_prompt_cmd"
|
||
EOF
|
||
```
|
||
|
||
---
|
||
|
||
## 4. Etape 3 : installer ttyd
|
||
|
||
Binaire statique, pas de compilation :
|
||
|
||
```bash
|
||
curl -fL -o /usr/local/bin/ttyd \
|
||
https://github.com/tsl0922/ttyd/releases/download/1.7.7/ttyd.x86_64
|
||
chmod +x /usr/local/bin/ttyd
|
||
ttyd --version # doit afficher 1.7.7
|
||
```
|
||
|
||
Verifier le nom exact de l'asset sur la page des releases si le telechargement echoue ;
|
||
les noms de fichiers changent selon les versions. Rester en **1.7.7** : le frontend
|
||
personnalise de l'etape 6 a ete corrige contre ce bundle xterm.js precis, une autre version
|
||
peut deplacer les points d'accroche.
|
||
|
||
Transfert de fichiers dans le navigateur (facultatif, active par `-t enableTrzsz=true`) :
|
||
|
||
```bash
|
||
curl -fL https://github.com/trzsz/trzsz-go/releases/download/v1.1.8/trzsz_1.1.8_linux_x86_64.tar.gz \
|
||
| tar xz -C /tmp
|
||
install -m755 /tmp/trzsz_1.1.8_linux_x86_64/tr[sz]* /usr/local/bin/
|
||
```
|
||
|
||
---
|
||
|
||
## 5. Etape 4 : session tmux persistante et service ttyd
|
||
|
||
### 5.1 Script de session
|
||
|
||
```bash
|
||
cat > /usr/local/bin/claude-session.sh <<'EOF'
|
||
#!/bin/bash
|
||
# Maintient la session tmux "claude" toujours en vie
|
||
|
||
tmux has-session -t claude 2>/dev/null || \
|
||
tmux new-session -d -s claude -c /root -x 220 -y 50
|
||
|
||
echo "[claude-session] Session tmux 'claude' active"
|
||
|
||
while true; do
|
||
if ! tmux has-session -t claude 2>/dev/null; then
|
||
echo "[claude-session] Session perdue, recreation..."
|
||
tmux new-session -d -s claude -c /root -x 220 -y 50
|
||
fi
|
||
sleep 5
|
||
done
|
||
EOF
|
||
chmod +x /usr/local/bin/claude-session.sh
|
||
```
|
||
|
||
Les dimensions `-x 220 -y 50` ne sont qu'une valeur de depart : le premier client qui
|
||
s'attache redimensionne la session.
|
||
|
||
### 5.2 Script de lancement ttyd
|
||
|
||
```bash
|
||
cat > /usr/local/bin/claude-start.sh <<'EOF'
|
||
#!/bin/bash
|
||
export LANG=fr_FR.UTF-8
|
||
export LC_ALL=fr_FR.UTF-8
|
||
exec /usr/local/bin/ttyd --port 7681 --writable --index /etc/ttyd/index.html \
|
||
-t enableTrzsz=true tmux attach-session -t claude
|
||
EOF
|
||
chmod +x /usr/local/bin/claude-start.sh
|
||
```
|
||
|
||
Ne pas ajouter `--credential` ici : l'authentification est geree par le reverse proxy.
|
||
Si le serveur Claude est expose autrement que derriere le proxy, alors il faut la remettre.
|
||
|
||
### 5.3 Unites systemd
|
||
|
||
```bash
|
||
cat > /etc/systemd/system/claude-session.service <<'EOF'
|
||
[Unit]
|
||
Description=Claude Code - Session tmux persistante
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Environment=HOME=/root
|
||
ExecStart=/usr/local/bin/claude-session.sh
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
cat > /etc/systemd/system/ttyd.service <<'EOF'
|
||
[Unit]
|
||
Description=Claude Code - Web Terminal (ttyd)
|
||
After=claude-session.service
|
||
Requires=claude-session.service
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
Environment=HOME=/root
|
||
ExecStart=/usr/local/bin/claude-start.sh
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
systemctl daemon-reload
|
||
systemctl enable --now claude-session.service ttyd.service
|
||
systemctl status ttyd.service --no-pager
|
||
```
|
||
|
||
Le couple `After=` + `Requires=` garantit l'ordre au demarrage. La separation en deux unites
|
||
est **le point structurant** : `systemctl restart ttyd` ne touche pas au serveur tmux, donc la
|
||
session Claude survit a toutes les manipulations du frontend.
|
||
|
||
Test local avant d'aller plus loin :
|
||
|
||
```bash
|
||
curl -sI http://127.0.0.1:7681/ | head -3
|
||
```
|
||
|
||
---
|
||
|
||
## 6. Etape 5 : frontend personnalise
|
||
|
||
C'est ici que se trouve tout le travail d'ergonomie. Le frontend stock de ttyd est un terminal
|
||
nu : pas de barre d'outils, pas de themes, pas de recherche, copier-coller approximatif.
|
||
|
||
### 6.1 Ce que le bloc personnalise apporte
|
||
|
||
- barre d'outils fixe de 30 px : indicateur de connexion websocket, taille de police +/-,
|
||
effacer, recherche, selecteur de theme (5 themes), boutons de split et de navigation tmux
|
||
- taille de police et theme persistes en localStorage
|
||
- recherche dans l'historique du buffer, `Ctrl+F`, `Entree` / `Maj+Entree` pour naviguer
|
||
- menu contextuel au clic droit (copier, coller, rechercher, effacer, police)
|
||
- `Ctrl+C` copie s'il y a une selection, sinon envoie SIGINT ; `Ctrl+V` colle
|
||
- `Ctrl+molette` pour zoomer
|
||
- OSC 52 : le presse-papier de tmux remonte dans celui du navigateur
|
||
- OSC 777 : notification desktop quand une commande longue se termine, onglet en arriere-plan
|
||
- les liens cliquables s'ouvrent dans un nouvel onglet au lieu de remplacer la page
|
||
|
||
### 6.2 Installation
|
||
|
||
Recuperer d'abord le `index.html` stock que ttyd embarque, puis y injecter le bloc :
|
||
|
||
```bash
|
||
mkdir -p /etc/ttyd
|
||
# instance jetable sur un autre port, uniquement pour recuperer la page embarquee
|
||
/usr/local/bin/ttyd --port 7699 tmux ls >/dev/null 2>&1 & P=$!
|
||
sleep 1
|
||
curl -s http://127.0.0.1:7699/ > /etc/ttyd/index.stock.html
|
||
kill $P
|
||
wc -c /etc/ttyd/index.stock.html # 729693 octets attendus en 1.7.7
|
||
```
|
||
|
||
Le service `ttyd` en production n'a pas besoin d'etre arrete : le port est different.
|
||
|
||
Placer `ttyd-custom.html` (fourni avec ce document) dans `/root/`, puis :
|
||
|
||
```bash
|
||
python3 - <<'EOF'
|
||
stock = open('/etc/ttyd/index.stock.html', encoding='utf-8').read()
|
||
custom = open('/root/ttyd-custom.html', encoding='utf-8').read()
|
||
tail = '</body></html>'
|
||
assert stock.rstrip().endswith(tail), "structure du index.html stock inattendue"
|
||
out = stock.rstrip()[:-len(tail)] + custom + tail + '\n'
|
||
open('/etc/ttyd/index.html','w',encoding='utf-8').write(out)
|
||
print(len(out), 'octets ecrits')
|
||
EOF
|
||
```
|
||
|
||
Verifier la syntaxe avant de recharger la page (une erreur JS ici donne un terminal blanc) :
|
||
|
||
```bash
|
||
python3 -c "
|
||
s=open('/etc/ttyd/index.html',encoding='utf-8').read()
|
||
js=s[s.rindex('<script>')+8:s.rindex('</script>')]
|
||
open('/tmp/check.js','w',encoding='utf-8').write(js)"
|
||
node --check /tmp/check.js && echo "JS OK"
|
||
```
|
||
|
||
**Aucun redemarrage n'est necessaire.** ttyd relit le fichier `--index` sur le disque a chaque
|
||
requete HTTP et repond `cache-control: no-store` : un simple rechargement de page suffit, la
|
||
session n'est jamais coupee. Toujours garder une sauvegarde avant modification :
|
||
|
||
```bash
|
||
cp /etc/ttyd/index.html /etc/ttyd/index.html.bak-$(date +%Y%m%d)
|
||
```
|
||
|
||
### 6.3 Contenu de `ttyd-custom.html`
|
||
|
||
Si le fichier joint est absent, creer `/root/ttyd-custom.html` avec exactement ce contenu.
|
||
Les sequences `\x02` (prefixe tmux Ctrl+B) et `\x1b` (ESC) sont des echappements JavaScript
|
||
volontaires : ne pas les convertir en caracteres bruts.
|
||
|
||
```html
|
||
<style>
|
||
#ttyd-bar{position:fixed;top:0;left:0;right:0;height:30px;background:#111;display:flex;align-items:center;padding:0 6px;gap:4px;z-index:8000;border-bottom:1px solid #2a2a2a;}
|
||
#ttyd-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;white-space:nowrap;}
|
||
#ttyd-bar button:hover{background:#333;color:#fff;}
|
||
#ttyd-bar select{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 4px;font:11px monospace;cursor:pointer;}
|
||
#ttyd-conn{width:8px;height:8px;border-radius:50%;background:#555;margin:0 4px;flex-shrink:0;}
|
||
#ttyd-conn.ok{background:#4caf50;}
|
||
#ttyd-conn.err{background:#f44336;animation:blink 1s infinite;}
|
||
@keyframes blink{0%,100%{opacity:1}50%{opacity:.3}}
|
||
#ttyd-spacer{flex:1;}
|
||
#ttyd-search-bar{position:fixed;top:30px;right:0;background:#1a1a1a;border:1px solid #333;border-top:none;border-right:none;border-radius:0 0 0 6px;display:flex;align-items:center;gap:4px;padding:4px 8px;z-index:8001;display:none;}
|
||
#ttyd-search-bar input{background:#111;color:#ddd;border:1px solid #333;border-radius:3px;padding:2px 6px;font:12px monospace;width:200px;outline:none;}
|
||
#ttyd-search-bar span{color:#666;font:11px monospace;min-width:60px;}
|
||
#ttyd-search-bar button{background:#222;color:#aaa;border:1px solid #333;border-radius:3px;padding:1px 7px;cursor:pointer;font:11px monospace;}
|
||
#terminal-container{position:absolute !important;top:30px;left:0;right:0;bottom:0;height:auto !important;padding-top:0 !important;}
|
||
#ttyd-ctx{position:fixed;background:#1e1e1e;border:1px solid #444;border-radius:4px;padding:4px 0;z-index:9999;min-width:150px;box-shadow:0 4px 12px rgba(0,0,0,.5);display:none;}
|
||
#ttyd-ctx div{padding:5px 14px;cursor:pointer;color:#ccc;font:12px monospace;}
|
||
#ttyd-ctx div:hover{background:#2d2d2d;color:#fff;}
|
||
#ttyd-ctx hr{border:none;border-top:1px solid #333;margin:3px 0;}
|
||
#ttyd-toast{position:fixed;bottom:8px;right:8px;background:rgba(20,20,20,.93);color:#7fff7f;font:12px monospace;padding:4px 10px;border-radius:4px;z-index:9999;pointer-events:none;opacity:0;transition:opacity .2s;}
|
||
#ttyd-toast.show{opacity:1;}
|
||
</style>
|
||
<div id="ttyd-bar">
|
||
<div id="ttyd-conn" title="Connexion WebSocket"></div>
|
||
<button onclick="ttydFontSize(-1)" title="Police -">A−</button>
|
||
<span id="ttyd-fsize" style="color:#555;font:11px monospace;min-width:28px;text-align:center;">13</span>
|
||
<button onclick="ttydFontSize(+1)" title="Police +">A+</button>
|
||
<button onclick="ttydClear()" title="Vider">✕ Clear</button>
|
||
<button onclick="ttydSearch()" title="Rechercher (Ctrl+F)">⌕ Rech.</button>
|
||
<select id="ttyd-theme-sel" onchange="ttydSetTheme(this.value)" title="Thème"></select>
|
||
<button onclick="ttydSplit('h')" title="Split vertical (côte à côte)">⬜ ⬜</button>
|
||
<button onclick="ttydSplit('v')" title="Split horizontal (haut/bas)">⬜<br style='line-height:3px'>⬜</button>
|
||
<button onclick="ttydPaneClose()" title="Fermer ce panneau">✕ Pan.</button>
|
||
<button onclick="ttydPaneNav('Left')" title="Panneau gauche">◀</button>
|
||
<button onclick="ttydPaneNav('Right')" title="Panneau droite">▶</button>
|
||
<button onclick="ttydPaneNav('Up')" title="Panneau haut">▲</button>
|
||
<button onclick="ttydPaneNav('Down')" title="Panneau bas">▼</button>
|
||
<div id="ttyd-spacer"></div>
|
||
<span style="color:#333;font:10px monospace;">Ctrl+C=copier/inter. Ctrl+V=coller clic droit=menu</span>
|
||
</div>
|
||
<div id="ttyd-search-bar">
|
||
<input id="ttyd-sq" placeholder="Rechercher…" oninput="ttydDoSearch()" onkeydown="ttydSearchKey(event)">
|
||
<span id="ttyd-sr">—</span>
|
||
<button onclick="ttydSearchNav(-1)">▲</button>
|
||
<button onclick="ttydSearchNav(+1)">▼</button>
|
||
<button onclick="ttydSearchClose()">✕</button>
|
||
</div>
|
||
<div id="ttyd-ctx"></div>
|
||
<div id="ttyd-toast"></div>
|
||
<script>
|
||
(function(){
|
||
var _fontSize = parseInt(localStorage.getItem('ttyd-font') || '13');
|
||
var _themes = {"Defaut": {"background": "#2b2b2b", "foreground": "#d2d2d2", "cursor": "#adadad", "black": "#000000", "red": "#d81e00", "green": "#5ea702", "yellow": "#cfae00", "blue": "#427ab3", "magenta": "#89658e", "cyan": "#00a7aa", "white": "#dbded8", "brightBlack": "#686a66", "brightRed": "#f54235", "brightGreen": "#99e343", "brightYellow": "#fdeb61", "brightBlue": "#84b0d8", "brightMagenta": "#bc94b7", "brightCyan": "#37e6e8", "brightWhite": "#f1f1f0"}, "Dracula": {"background": "#282a36", "foreground": "#f8f8f2", "cursor": "#f8f8f2", "black": "#21222c", "red": "#ff5555", "green": "#50fa7b", "yellow": "#f1fa8c", "blue": "#bd93f9", "magenta": "#ff79c6", "cyan": "#8be9fd", "white": "#f8f8f2", "brightBlack": "#6272a4", "brightRed": "#ff6e6e", "brightGreen": "#69ff94", "brightYellow": "#ffffa5", "brightBlue": "#d6acff", "brightMagenta": "#ff92df", "brightCyan": "#a4ffff", "brightWhite": "#ffffff"}, "One Dark": {"background": "#282c34", "foreground": "#abb2bf", "cursor": "#528bff", "black": "#3f4451", "red": "#e06c75", "green": "#98c379", "yellow": "#e5c07b", "blue": "#61afef", "magenta": "#c678dd", "cyan": "#56b6c2", "white": "#abb2bf", "brightBlack": "#4f5666", "brightRed": "#ff616e", "brightGreen": "#a5e075", "brightYellow": "#f0a45d", "brightBlue": "#4dc4ff", "brightMagenta": "#de73ff", "brightCyan": "#4cd1e0", "brightWhite": "#ffffff"}, "Solarized": {"background": "#002b36", "foreground": "#839496", "cursor": "#839496", "black": "#073642", "red": "#dc322f", "green": "#859900", "yellow": "#b58900", "blue": "#268bd2", "magenta": "#d33682", "cyan": "#2aa198", "white": "#eee8d5", "brightBlack": "#002b36", "brightRed": "#cb4b16", "brightGreen": "#586e75", "brightYellow": "#657b83", "brightBlue": "#839496", "brightMagenta": "#6c71c4", "brightCyan": "#93a1a1", "brightWhite": "#fdf6e3"}, "Clair": {"background": "#ffffff", "foreground": "#383a42", "cursor": "#526fff", "black": "#383a42", "red": "#e45649", "green": "#50a14f", "yellow": "#c18401", "blue": "#4078f2", "magenta": "#a626a4", "cyan": "#0184bc", "white": "#fafafa", "brightBlack": "#4f525e", "brightRed": "#e06c75", "brightGreen": "#98c379", "brightYellow": "#e5c07b", "brightBlue": "#61afef", "brightMagenta": "#c678dd", "brightCyan": "#56b6c2", "brightWhite": "#ffffff"}};
|
||
var _themeName = localStorage.getItem('ttyd-theme') || 'Defaut';
|
||
var _searchMatches = [], _searchIdx = 0;
|
||
var _notifPerm = false;
|
||
var _lastCmdStart = 0;
|
||
|
||
/* ---- Toast ---- */
|
||
function toast(msg, dur) {
|
||
var t = document.getElementById('ttyd-toast');
|
||
t.textContent = msg; t.classList.add('show');
|
||
clearTimeout(t._tid);
|
||
t._tid = setTimeout(function(){ t.classList.remove('show'); }, dur||1800);
|
||
}
|
||
|
||
/* ---- Clipboard ---- */
|
||
function writeClip(text) {
|
||
if (!text) return;
|
||
var ok = function(){ toast('Copié : ' + text.slice(0,40).replace(/\n/g,' ') + (text.length>40?'..':'')); };
|
||
if (navigator.clipboard && navigator.clipboard.writeText)
|
||
navigator.clipboard.writeText(text).then(ok).catch(function(){ legacyCopy(text); });
|
||
else legacyCopy(text);
|
||
}
|
||
function legacyCopy(text) {
|
||
var ta = document.createElement('textarea');
|
||
ta.value = text; ta.style.cssText='position:fixed;top:0;left:0;width:1px;height:1px;opacity:0;';
|
||
document.body.appendChild(ta); ta.focus(); ta.select();
|
||
var ok=false; try{ ok=document.execCommand('copy'); }catch(e){}
|
||
document.body.removeChild(ta);
|
||
toast(ok ? 'Copié (legacy)' : 'Erreur copie');
|
||
}
|
||
function readClip(cb) {
|
||
if (navigator.clipboard && navigator.clipboard.readText)
|
||
navigator.clipboard.readText().then(cb).catch(function(){ toast('Accès presse-papier refusé'); });
|
||
else toast('API clipboard non disponible');
|
||
}
|
||
|
||
/* ---- Police ---- */
|
||
window.ttydFontSize = function(d) {
|
||
_fontSize = Math.max(8, Math.min(28, _fontSize + d));
|
||
localStorage.setItem('ttyd-font', _fontSize);
|
||
if (window.term) { window.term.options.fontSize = _fontSize; window.dispatchEvent(new Event('resize')); }
|
||
document.getElementById('ttyd-fsize').textContent = _fontSize;
|
||
};
|
||
|
||
/* ---- Clear ---- */
|
||
window.ttydClear = function() { if (window.term) window.term.clear(); };
|
||
|
||
/* ---- Tmux split & navigation ---- */
|
||
function sendRaw(data) {
|
||
// Envoie données brutes au PTY sans bracketed paste
|
||
var term = window.term;
|
||
if (!term) return;
|
||
if (term._core && term._core.coreService)
|
||
term._core.coreService.triggerDataEvent(data, true);
|
||
else
|
||
term.paste(data);
|
||
}
|
||
window.ttydSplit = function(dir) {
|
||
// Ctrl+B (\x02) + % pour vertical, + " pour horizontal
|
||
sendRaw('\x02' + (dir === 'h' ? '%' : '"'));
|
||
};
|
||
window.ttydPaneClose = function() {
|
||
// Ctrl+B + x, puis y pour confirmer
|
||
sendRaw('\x02x');
|
||
setTimeout(function(){ sendRaw('y'); }, 200);
|
||
};
|
||
window.ttydPaneNav = function(dir) {
|
||
var arrows = {Left:'\x1b[D', Right:'\x1b[C', Up:'\x1b[A', Down:'\x1b[B'};
|
||
if (arrows[dir]) sendRaw('\x02' + arrows[dir]);
|
||
};
|
||
|
||
/* ---- Thèmes ---- */
|
||
var sel = document.getElementById('ttyd-theme-sel');
|
||
Object.keys(_themes).forEach(function(n){
|
||
var o = document.createElement('option'); o.value=n; o.textContent=n;
|
||
if(n===_themeName) o.selected=true;
|
||
sel.appendChild(o);
|
||
});
|
||
window.ttydSetTheme = function(name) {
|
||
if (!_themes[name] || !window.term) return;
|
||
window.term.options.theme = _themes[name];
|
||
_themeName = name;
|
||
localStorage.setItem('ttyd-theme', name);
|
||
toast('Thème : ' + name);
|
||
};
|
||
|
||
/* ---- Recherche ---- */
|
||
var _searchOpen = false;
|
||
window.ttydSearch = function() {
|
||
var bar = document.getElementById('ttyd-search-bar');
|
||
_searchOpen = !_searchOpen;
|
||
bar.style.display = _searchOpen ? 'flex' : 'none';
|
||
if (_searchOpen) setTimeout(function(){ document.getElementById('ttyd-sq').focus(); }, 50);
|
||
};
|
||
window.ttydSearchClose = function() {
|
||
_searchOpen = false;
|
||
document.getElementById('ttyd-search-bar').style.display = 'none';
|
||
_searchMatches = []; document.getElementById('ttyd-sr').textContent = '—';
|
||
};
|
||
window.ttydDoSearch = function() {
|
||
var q = document.getElementById('ttyd-sq').value;
|
||
var sr = document.getElementById('ttyd-sr');
|
||
if (!q || !window.term) { _searchMatches=[]; sr.textContent='—'; return; }
|
||
var buf = window.term.buffer.active;
|
||
var matches = [];
|
||
var total = buf.length;
|
||
for (var i = 0; i < total; i++) {
|
||
var line = buf.getLine(i);
|
||
if (line && line.translateToString().indexOf(q) >= 0) matches.push(i);
|
||
}
|
||
_searchMatches = matches; _searchIdx = matches.length - 1;
|
||
sr.textContent = matches.length ? ((_searchIdx+1)+'/'+matches.length) : '0 résultat';
|
||
if (matches.length) window.term.scrollToLine(matches[_searchIdx]);
|
||
};
|
||
window.ttydSearchNav = function(dir) {
|
||
if (!_searchMatches.length) return;
|
||
_searchIdx = (_searchIdx + dir + _searchMatches.length) % _searchMatches.length;
|
||
document.getElementById('ttyd-sr').textContent = (_searchIdx+1)+'/'+_searchMatches.length;
|
||
window.term.scrollToLine(_searchMatches[_searchIdx]);
|
||
};
|
||
window.ttydSearchKey = function(e) {
|
||
if (e.key === 'Enter') ttydSearchNav(e.shiftKey ? -1 : 1);
|
||
if (e.key === 'Escape') ttydSearchClose();
|
||
};
|
||
|
||
/* ---- Connexion ---- */
|
||
var _connEl = document.getElementById('ttyd-conn');
|
||
function checkConn() {
|
||
fetch('/token', {cache:'no-store'})
|
||
.then(function(r){ _connEl.className = r.ok ? 'ok' : 'err'; })
|
||
.catch(function(){ _connEl.className = 'err'; });
|
||
}
|
||
checkConn();
|
||
setInterval(checkConn, 5000);
|
||
|
||
/* ---- Patch window.open (liens WebLinksAddon) ---- */
|
||
var _open = window.open;
|
||
window.open = function(url, target, features) {
|
||
if (url === undefined || url === null || url === '') {
|
||
var loc = {};
|
||
Object.defineProperty(loc,'href',{set:function(u){
|
||
var a=document.createElement('a'); a.href=u; a.target='_blank'; a.rel='noopener noreferrer';
|
||
document.body.appendChild(a); a.click(); document.body.removeChild(a);
|
||
}});
|
||
return {opener:null, location:loc};
|
||
}
|
||
return _open.apply(this, arguments);
|
||
};
|
||
|
||
/* ---- Menu contextuel ---- */
|
||
var _ctx = document.getElementById('ttyd-ctx');
|
||
function hideCtx(){ _ctx.style.display='none'; }
|
||
document.addEventListener('click', hideCtx);
|
||
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ hideCtx(); ttydSearchClose(); }});
|
||
function showCtx(x,y) {
|
||
hideCtx();
|
||
_ctx.innerHTML = '';
|
||
var items = [
|
||
['Copier', function(){ if(window.term){var s=window.term.getSelection();if(s)writeClip(s);} }],
|
||
['Coller', function(){ if(window.term) readClip(function(t){window.term.paste(t);}); }],
|
||
null,
|
||
['Rechercher…', ttydSearch],
|
||
null,
|
||
['Effacer', ttydClear],
|
||
['Police +', function(){ttydFontSize(+1);}],
|
||
['Police −', function(){ttydFontSize(-1);}],
|
||
];
|
||
items.forEach(function(item){
|
||
if(!item){var hr=document.createElement('hr');_ctx.appendChild(hr);return;}
|
||
var d=document.createElement('div'); d.textContent=item[0];
|
||
d.addEventListener('click',function(e){e.stopPropagation();hideCtx();item[1]();});
|
||
_ctx.appendChild(d);
|
||
});
|
||
_ctx.style.cssText='display:block;left:'+x+'px;top:'+y+'px;';
|
||
var r=_ctx.getBoundingClientRect();
|
||
if(r.right>window.innerWidth) _ctx.style.left=(x-r.width)+'px';
|
||
if(r.bottom>window.innerHeight) _ctx.style.top=(y-r.height)+'px';
|
||
}
|
||
|
||
/* ---- Notifications ---- */
|
||
function requestNotifPerm() {
|
||
if ('Notification' in window && Notification.permission === 'default')
|
||
Notification.requestPermission().then(function(p){ _notifPerm = p==='granted'; });
|
||
else _notifPerm = ('Notification' in window && Notification.permission === 'granted');
|
||
}
|
||
function desktopNotif(title, body) {
|
||
if (_notifPerm && 'Notification' in window)
|
||
new Notification(title, {body:body, icon:''});
|
||
}
|
||
requestNotifPerm();
|
||
|
||
/* ---- Initialisation terminal ---- */
|
||
function waitForTerm() {
|
||
var term = window.term;
|
||
if (!term || !term.parser) { setTimeout(waitForTerm, 100); return; }
|
||
|
||
/* Police initiale */
|
||
term.options.fontSize = _fontSize;
|
||
document.getElementById('ttyd-fsize').textContent = _fontSize;
|
||
|
||
/* Thème initial */
|
||
if (_themes[_themeName]) term.options.theme = _themes[_themeName];
|
||
|
||
/* Refit : le fit initial a ete calcule avec l'ancienne taille de cellule */
|
||
setTimeout(function(){ window.dispatchEvent(new Event('resize')); }, 50);
|
||
|
||
/* OSC 52 : tmux set-clipboard -> presse-papier */
|
||
try {
|
||
term.parser.registerOscHandler(52, function(data) {
|
||
var parts = data.split(';');
|
||
if (parts.length>=2 && parts[1] && parts[1]!=='?') {
|
||
try { writeClip(atob(parts[1])); } catch(e){}
|
||
}
|
||
return true;
|
||
});
|
||
} catch(e){}
|
||
|
||
/* OSC 777 : PROMPT_COMMAND -> notification fin de commande */
|
||
try {
|
||
term.parser.registerOscHandler(777, function(data) {
|
||
var code = parseInt(data) || 0;
|
||
var elapsed = Date.now() - _lastCmdStart;
|
||
/* Notifier si onglet caché ET commande > 5s */
|
||
if (document.hidden && elapsed > 5000) {
|
||
var msg = code === 0 ? 'Commande terminée (succès)' : 'Commande terminée (code '+code+')';
|
||
desktopNotif('Terminal', msg);
|
||
}
|
||
_lastCmdStart = Date.now();
|
||
return true;
|
||
});
|
||
} catch(e){}
|
||
|
||
/* Ctrl+C = copie si sélection, sinon SIGINT */
|
||
/* Ctrl+V = colle */
|
||
/* Ctrl+F = recherche */
|
||
term.attachCustomKeyEventHandler(function(e) {
|
||
if (e.type !== 'keydown') return true;
|
||
if (e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
|
||
if (e.code==='KeyC') {
|
||
var s = term.getSelection ? term.getSelection() : '';
|
||
if (s) { writeClip(s); return false; }
|
||
return true;
|
||
}
|
||
if (e.code==='KeyV') {
|
||
e.preventDefault();
|
||
readClip(function(t){ term.paste(t); }); return false;
|
||
}
|
||
if (e.code==='KeyF') { ttydSearch(); return false; }
|
||
}
|
||
if (e.ctrlKey && e.shiftKey) {
|
||
if (e.code==='KeyC') { var s=term.getSelection?term.getSelection():''; if(s) writeClip(s); return false; }
|
||
if (e.code==='KeyV') { e.preventDefault(); readClip(function(t){ term.paste(t); }); return false; }
|
||
}
|
||
return true;
|
||
});
|
||
|
||
/* Clic droit -> menu contextuel */
|
||
term.element.addEventListener('contextmenu', function(e){
|
||
e.preventDefault(); showCtx(e.clientX, e.clientY);
|
||
});
|
||
|
||
/* Zoom souris */
|
||
term.element.addEventListener('wheel', function(e){
|
||
if (!e.ctrlKey) return;
|
||
e.preventDefault();
|
||
ttydFontSize(e.deltaY < 0 ? 1 : -1);
|
||
}, {passive:false});
|
||
}
|
||
waitForTerm();
|
||
|
||
|
||
})();
|
||
</script>
|
||
```
|
||
|
||
---
|
||
|
||
## 7. Etape 6 : reverse proxy, domaine et TLS
|
||
|
||
Caddy est le choix retenu : HTTPS automatique par Let's Encrypt, websocket natif sans
|
||
configuration, fichier de conf de trois lignes. Nginx ou Traefik fonctionnent aussi, mais
|
||
demandent une configuration websocket explicite (`Upgrade` / `Connection`), source classique
|
||
de terminal qui ne se connecte pas.
|
||
|
||
### 7.1 Installer Caddy
|
||
|
||
Sur une machine dediee (ex. `192.168.1.2`) ou sur le serveur Claude lui-meme :
|
||
|
||
```bash
|
||
apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
|
||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
|
||
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
|
||
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
|
||
| tee /etc/apt/sources.list.d/caddy-stable.list
|
||
apt update && apt install -y caddy
|
||
caddy version
|
||
```
|
||
|
||
### 7.2 Generer le hash du mot de passe
|
||
|
||
```bash
|
||
caddy hash-password
|
||
# saisir le mot de passe deux fois, copier le hash bcrypt affiche ($2a$14$...)
|
||
```
|
||
|
||
Stocker le mot de passe en clair dans un gestionnaire de mots de passe : le hash bcrypt n'est
|
||
pas reversible, un mot de passe perdu impose d'en regenerer un.
|
||
|
||
### 7.3 Caddyfile
|
||
|
||
```
|
||
claude.mondomaine.fr {
|
||
basic_auth {
|
||
claude $2a$14$REMPLACER_PAR_LE_HASH_GENERE
|
||
}
|
||
reverse_proxy 192.168.1.123:7681
|
||
}
|
||
```
|
||
|
||
```bash
|
||
caddy validate --config /etc/caddy/Caddyfile
|
||
systemctl reload caddy
|
||
```
|
||
|
||
Notes :
|
||
|
||
- `basic_auth` est la directive de Caddy >= 2.8. Sur une version anterieure, ecrire `basicauth`.
|
||
- Le hash contient des `$`. Dans un Caddyfile ce n'est pas un probleme, mais si tu generes ce
|
||
fichier depuis un script shell, protege-le des expansions (guillemets simples).
|
||
- Les websockets passent seuls : ne surtout pas ajouter de `header_up Upgrade` manuel.
|
||
- Si le proxy et le serveur Claude sont sur la meme machine, remplacer par
|
||
`reverse_proxy 127.0.0.1:7681`.
|
||
|
||
### 7.4 DNS et ouverture de ports
|
||
|
||
1. Enregistrement DNS public : `claude.mondomaine.fr` -> IP publique (type A). Si l'IP est
|
||
dynamique, utiliser un client DDNS.
|
||
2. Redirection sur le routeur : **80 -> proxy:80** et **443 -> proxy:443**. Rien d'autre.
|
||
Le port 80 est requis pour la validation Let's Encrypt en HTTP-01.
|
||
3. **Ne jamais rediriger le port 7681.** Il donne un shell root sans authentification.
|
||
4. Si le reseau local resout deja `*.mondomaine.fr`, ajouter une entree interne
|
||
`claude.mondomaine.fr -> 192.168.1.2` pour eviter de sortir sur internet.
|
||
5. Pare-feu du serveur Claude : n'autoriser 7681 que depuis l'IP du proxy.
|
||
|
||
```bash
|
||
# sur le serveur claude-code, si ufw est utilise
|
||
ufw allow from 192.168.1.2 to any port 7681 proto tcp
|
||
ufw deny 7681
|
||
```
|
||
|
||
Le premier acces en HTTPS declenche l'emission du certificat, ce qui prend quelques secondes.
|
||
En cas d'echec, `journalctl -u caddy -f` donne la raison exacte (port 80 non joignable depuis
|
||
l'exterieur dans 90 % des cas).
|
||
|
||
---
|
||
|
||
## 8. Verification
|
||
|
||
Dans l'ordre, en s'arretant a la premiere etape qui echoue :
|
||
|
||
```bash
|
||
# 1. ttyd repond en local
|
||
curl -sI http://127.0.0.1:7681/ | head -1 # 200
|
||
|
||
# 2. la session tmux existe
|
||
tmux ls # claude: 1 windows
|
||
|
||
# 3. le proxy demande bien une authentification
|
||
curl -sI https://claude.mondomaine.fr | head -1 # 401
|
||
|
||
# 4. avec les identifiants
|
||
curl -sI -u claude:MOTDEPASSE https://claude.mondomaine.fr | head -1 # 200
|
||
```
|
||
|
||
Puis dans un navigateur, controler point par point :
|
||
|
||
- [ ] le terminal occupe toute la hauteur, la derniere ligne n'est pas coupee
|
||
- [ ] la pastille de connexion en haut a gauche est verte
|
||
- [ ] `Ctrl+V` colle **une seule fois** le contenu du presse-papier
|
||
- [ ] selectionner du texte puis `Ctrl+C` affiche le toast "Copie" et le texte est
|
||
recuperable dans une autre application
|
||
- [ ] les boutons A+ / A- changent la taille **et** la grille se recalcule (le nombre de
|
||
lignes visibles change)
|
||
- [ ] le clic droit ouvre le menu contextuel
|
||
- [ ] fermer l'onglet, le rouvrir : la session est retrouvee intacte
|
||
- [ ] `systemctl restart ttyd` puis recharger : la session est toujours la
|
||
|
||
---
|
||
|
||
## 9. Pieges connus et bugs deja corriges
|
||
|
||
Cette section est la raison d'etre du document. Ces defauts ont tous ete rencontres et
|
||
diagnostiques en production ; les correctifs sont **deja integres** dans `ttyd-custom.html`.
|
||
Elle sert a ne pas les reintroduire lors d'une modification ulterieure.
|
||
|
||
### 9.1 Bas du terminal tronque (deux causes cumulees)
|
||
|
||
**Cause A : le padding du parent est compte comme espace disponible par le FitAddon.**
|
||
Poser `#terminal-container{padding-top:30px}` pour degager la barre d'outils semble naturel.
|
||
C'est structurellement faux. `proposeDimensions()` du FitAddon calcule :
|
||
|
||
```js
|
||
r = parseInt(getComputedStyle(parentElement).getPropertyValue("height"))
|
||
a = r - (padding-top + padding-bottom de .terminal) // le padding du PARENT n'est jamais retranche
|
||
rows = floor(a / cell.height)
|
||
```
|
||
|
||
Il mesure donc 30 px de trop, soit environ deux lignes de trop a chaque fit.
|
||
|
||
Regle : **ne jamais poser de padding, de bordure ou de decoration sur `#terminal-container`.**
|
||
C'est l'element mesure par le FitAddon, il doit contenir exactement l'espace utilisable.
|
||
Pour degager de la place, positionner le conteneur, ne pas le rembourrer :
|
||
|
||
```css
|
||
#terminal-container{position:absolute !important;top:30px;left:0;right:0;bottom:0;
|
||
height:auto !important;padding-top:0 !important;}
|
||
```
|
||
|
||
**Cause B : police changee apres l'initialisation, sans refit.** Le frontend applique la
|
||
taille de police memorisee en localStorage alors que le fit initial a deja tourne avec la
|
||
cellule d'origine, plus petite, donc avec trop de lignes. De plus, `term.fit()` **n'existe
|
||
pas** sur un Terminal xterm.js : `fit()` appartient au FitAddon. L'appel est ignore
|
||
silencieusement et les boutons A+ / A- ne recalculent jamais la grille.
|
||
|
||
Regle : **tout changement de `term.options.fontSize` doit etre suivi de**
|
||
|
||
```js
|
||
window.dispatchEvent(new Event('resize'));
|
||
```
|
||
|
||
C'est l'evenement que ttyd ecoute reellement (`g(window,"resize",()=>t.fit())` dans son
|
||
bundle). Le meme appel est place en differe de 50 ms apres l'application de la police
|
||
initiale, dans `waitForTerm()`.
|
||
|
||
Mesures reelles avant / apres, fenetre de 980 px, police 14, dpr 1.25 :
|
||
|
||
| | padding conteneur | rows | bas de .xterm-screen |
|
||
|---|---|---|---|
|
||
| avant, au chargement | 30 | 62 | 1027 (deborde de 69 px) |
|
||
| apres, au chargement | 0 | 61 | 1011 |
|
||
| apres, refit a +50 ms | 0 | 58 | **963, tient dans 980** |
|
||
|
||
### 9.2 Ctrl+V collait en double
|
||
|
||
Le gestionnaire de touches faisait `readClip(t => term.paste(t)); return false;` sans
|
||
`preventDefault()`. `return false` empeche seulement xterm.js de traiter la touche : le
|
||
navigateur emettait quand meme son evenement `paste` natif vers le textarea cache, ou xterm
|
||
l'ecoute (`handlePasteEvent` -> `getData("text/plain")` -> `paste`). Deux insertions.
|
||
|
||
Correctif : `e.preventDefault()` dans les branches `Ctrl+V` et `Ctrl+Maj+V`. `return false`
|
||
est conserve pour que xterm n'envoie pas `\x16` au PTY. Le seul chemin de collage restant est
|
||
`readClip`.
|
||
|
||
```js
|
||
if (e.code==='KeyV') {
|
||
e.preventDefault();
|
||
readClip(function(t){ term.paste(t); });
|
||
return false;
|
||
}
|
||
```
|
||
|
||
Meme defaut latent sur la branche `Ctrl+C`, laisse tel quel car sans consequence visible :
|
||
la copie native et `writeClip` copient la meme selection.
|
||
|
||
### 9.3 Copier depuis le terminal distant ne remontait pas
|
||
|
||
Il faut les deux moities : `set -g set-clipboard on` cote tmux (emission de l'OSC 52) et le
|
||
handler `registerOscHandler(52, ...)` cote frontend, qui decode le base64 et appelle
|
||
`navigator.clipboard.writeText`. L'un sans l'autre ne donne rien.
|
||
|
||
`navigator.clipboard` n'existe qu'en contexte securise : cela fonctionne en HTTPS ou sur
|
||
`localhost`, **jamais en HTTP simple sur une IP**. C'est une raison de plus de toujours passer
|
||
par le proxy TLS, meme pour tester. Un repli `document.execCommand('copy')` est present pour
|
||
les navigateurs anciens.
|
||
|
||
### 9.4 Les liens ouvraient une page blanche
|
||
|
||
Le WebLinksAddon appelle `window.open('')` puis assigne `location.href`, comportement que les
|
||
navigateurs recents bloquent ou traitent mal. Le bloc personnalise remplace `window.open` :
|
||
quand l'URL est vide, il retourne un objet dont le setter `href` cree un `<a target="_blank"
|
||
rel="noopener noreferrer">` et le clique. Ne pas retirer ce patch.
|
||
|
||
### 9.5 Pieges d'exploitation
|
||
|
||
- **Ne pas mettre l'authentification a deux endroits.** ttyd `--credential` **et** basic_auth
|
||
Caddy donnent une double invite. Une seule, au niveau du proxy.
|
||
- **Modifier `index.html` ne demande pas de redemarrage.** ttyd relit le fichier a chaque
|
||
requete. Redemarrer ttyd est neanmoins sans danger pour la session (elle appartient a
|
||
l'autre unite systemd).
|
||
- **Une erreur de syntaxe JS donne un terminal blanc et muet.** Toujours passer
|
||
`node --check` avant de recharger, et garder la sauvegarde datee.
|
||
- **Ne pas suivre les mises a jour de ttyd sans verifier.** Le bloc personnalise s'accroche a
|
||
des details du bundle xterm.js de la 1.7.7 (`window.term`, l'ecoute de `resize`,
|
||
`#terminal-container`). Apres une montee de version, rejouer toute la checklist du 8.
|
||
- **Methode de debug du frontend, sans rien installer.** ttyd journalise le **chemin** de
|
||
chaque requete HTTP. Il suffit d'encoder les mesures dans un chemin bidon : ttyd repond 404
|
||
et la valeur atterrit dans le journal, lisible cote serveur.
|
||
|
||
```js
|
||
fetch('/diag/win'+innerHeight+'/rows'+term.rows+'/sBot'+rect.bottom);
|
||
```
|
||
```bash
|
||
journalctl -u ttyd -f | grep diag
|
||
```
|
||
|
||
Aucun endpoint a creer, aucun port a ouvrir, et le retour est lisible directement au lieu
|
||
d'etre dicte par l'utilisateur. Penser a retirer l'instrumentation ensuite.
|
||
- **Lecon de methode.** Sur le bug du bas tronque, la premiere hypothese, purement CSS, etait
|
||
fausse et n'a rien corrige. Le signal d'alerte a ete une prediction contredite par
|
||
l'observation. Sur ce frontend, mesurer avant de conclure coute moins cher que corriger au
|
||
jugé.
|
||
|
||
---
|
||
|
||
## 10. Options utiles
|
||
|
||
**Redemarrage propre de la session Claude** sans toucher aux services :
|
||
|
||
```bash
|
||
tmux send-keys -t claude C-c
|
||
tmux send-keys -t claude 'claude' Enter
|
||
```
|
||
|
||
**Editeur web** sur le meme serveur, pratique pour editer les fichiers de config :
|
||
|
||
```bash
|
||
curl -fsSL https://code-server.dev/install.sh | sh
|
||
systemctl enable --now code-server@root # port 8080
|
||
```
|
||
|
||
Ajouter un bloc Caddy dedie, avec la meme protection :
|
||
|
||
```
|
||
code.mondomaine.fr {
|
||
basic_auth { claude $2a$14$LE_HASH }
|
||
reverse_proxy 192.168.1.123:8080
|
||
}
|
||
```
|
||
|
||
**Sauvegarde de la configuration** : versionner `/etc/ttyd/index.html`, les unites systemd et
|
||
les scripts `/usr/local/bin/claude-*.sh` dans un depot git prive. C'est ce qui permet de
|
||
rejouer l'installation ou de revenir en arriere apres une modification ratee du frontend.
|
||
|
||
Attention en versionnant : filtrer tout secret avant le commit. Un fichier d'environnement
|
||
pousse par erreur reste dans l'historique meme apres correction, et la seule remediation
|
||
reelle est de **regenerer les identifiants concernes**.
|
||
|
||
---
|
||
|
||
## 11. Rappel de securite
|
||
|
||
`claude.mondomaine.fr` donne un **shell root persistant** sur le serveur, expose sur internet.
|
||
Le niveau de protection doit etre a la hauteur :
|
||
|
||
- mot de passe long et unique, stocke dans un gestionnaire de mots de passe
|
||
- port 7681 jamais redirige, et filtre au pare-feu sur l'IP du proxy
|
||
- HTTPS obligatoire, jamais d'acces en HTTP simple
|
||
- evolution recommandee : remplacer basic_auth par une authentification SSO avec second
|
||
facteur (forward_auth vers Authentik ou Authelia), ou restreindre l'acces a un VPN
|
||
- si un service d'upload de fichiers est ajoute a cote, verifier qu'il ne tourne pas avec un
|
||
mot de passe par defaut et qu'il n'ecoute pas sur `0.0.0.0` sans authentification
|