Files
budget-tracker/MEMORY.md
T
2026-03-12 17:22:45 +00:00

376 lines
20 KiB
Markdown
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.
# MEMORY.md — Nox 🌑
## 📋 Règle de mémorisation
| Critère | MEMORY.md | Qdrant | Les deux |
|---|---|---|---|
| Config infra (URL, token, IDs) | ✅ | ❌ | — |
| Préférences & comportements | ✅ | ❌ | — |
| Leçon courte / gotcha technique | ✅ | ✅ | ← les deux |
| Procédure détaillée / code | ❌ | ✅ | — |
| Événement épisodique (ce qu'on a fait) | ❌ | ✅ | — |
| Fait important ET recherchable | ✅ | ✅ | ← les deux |
**Résumé :**
- **MEMORY.md** = ce que je dois savoir DÈS le démarrage. Court, dense, stable.
- **Qdrant** = ce que je dois pouvoir RETROUVER par recherche. Détails, procédures, contexte.
- **Les deux** = les leçons critiques, les gotchas techniques, tout ce qui est à la fois essentiel ET recharchable.
**Toujours ajouter dans Qdrant** après toute session de travail significative.
**Ne pas surcharger MEMORY.md** — si c'est long ou procédural, Qdrant suffit.
## Christophe
- Habite Montlieu-La-Garde, Charente-Maritime (17), axe Bordeaux-Angoulême N10
- Fuseau horaire : Europe/Paris
- Langue : français
## Home Assistant
- URL : http://192.168.1.40:8123
- **Lumières** :
- `light.dimmer_2`**Entrée**
- `light.bar`**Bar**
- `light.bibliotheque`**Bibliothèque**
- `light.dimmer_salon`**Salon**
- `light.nano_dimmer`**Mezzanine** (Nano Dimmer Z-Wave)
- `light.mi_light_wifi_ibox1`**Mi-Light WiFi iBox1** (la "lumière wifi")
- **Caméras** :
- `camera.onvif_ptz`**Caméra Extérieur** (double vue, couleur, parking/cour)
- `camera.fi9821ep`**Caméra Salon** (Foscam, intérieur)
- `camera.camera_ndeg7`**Caméra n°7** (intérieur, sous-sol/atelier)
- `camera.camera_jarnac_rdc_7`**Caméra Jarnac RDC 7** (intérieur)
- `camera.foscam`**Foscam** (intérieur, entrée/pièce de vie)
- `camera.klipper_webcam`**Ender 3 Webcam** (imprimante 3D)
- `camera.predator_predator`**Predator**
- `camera.nono_none`**Nono**
- **Media Players** :
- `media_player.shield`**SHIELD CUISINE**
- `media_player.android_tv_cuisine`**Android TV Cuisine**
- `media_player.shield_salon`**SHIELD SALON**
- `media_player.android_tv_salon`**Android TV Salon**
- `media_player.denon_avr_x3400h`**Denon AVR-X3400H**
## Proxmox
- URL : https://192.168.1.250:8006
- Token : variable d'env `PVE_TOKEN` (format `root@pam!openclaw=<uuid>`) — déjà dans .env + docker-compose override
- Accès API : `curl -sk -H "Authorization: PVEAPIToken=$PVE_TOKEN" "$PVE_URL/api2/json/nodes"`
- **Parser JSON avec Node.js** (pas jq — permission denied dans le conteneur)
- Nodes : mini-pc, ts-651, pve, z820
## Proxmox Backup Server (PBS)
- URL : https://192.168.1.91:8007
- Token : variables d'env `PBS_TOKEN_ID` + `PBS_TOKEN_SECRET` — déjà dans .env + docker-compose override
- Accès API : `curl -sk -H "Authorization: PBSAPIToken=$PBS_TOKEN_ID:$PBS_TOKEN_SECRET" "$PBS_URL/api2/json/status/datastore-usage"`
- Datastore : `backups_on_ts651` (1.26 TB total)
## Outils & Préférences
- **Génération d'images** : utiliser fal.ai (FAL_KEY), PAS OpenAI
- `fal-ai/flux/schnell` — génération rapide, bon pour photos/art
- `fal-ai/nano-banana-pro` — Gemini 3 Pro Image, bon pour affiches/texte/édition d'images
- **Browser OpenClaw (outil `browser`)** : ✅ OPÉRATIONNEL — headless Chromium Playwright, profil `openclaw`
- Config : `attachOnly: false`, `executablePath: /home/node/.openclaw/workspace/chrome-wrapper.sh`
- Le `chrome-wrapper.sh` nettoie les lock files résiduels AVANT de lancer Chrome (fix permanent pour le bug SingletonLock après redémarrage)
- Chrome réel : `/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome` (appelé par le wrapper)
- CDP port : 18800, user-data-dir : `/home/node/.openclaw/browser/openclaw/user-data`
- Utiliser pour : snapshots ARIA, interactions UI, screenshots, sessions persistantes
- Se lance automatiquement au premier appel `browser action=start profile=openclaw`
- **Toujours sauvegarder les fichiers dans le workspace** — `/tmp` est bloqué pour l'envoi Telegram
- ⚠️ Si le browser ne répond plus : vérifier que Chrome tourne (`curl -sf http://127.0.0.1:18800/json/version`) — relancer via `bash /home/node/.openclaw/workspace/start-chrome-cdp.sh`
- **Playwright direct (Node.js)** : pour interception réseau, scripts sur mesure, scraping avancé
- Installé dans `/home/node/.openclaw/workspace/node_modules/playwright`
- `executablePath``/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome`
- Les erreurs `dbus` en conteneur sont normales et sans impact
- **TTS** : Edge, voix fr-FR-VivienneMultilingualNeural — **TOUJOURS parler en français**, même si la conversation contient de l'anglais
- **Transcription audio** : Groq Whisper
## Anytype
- Instance self-hosted : http://192.168.1.150:31009
- Espace principal : **OpenClaw** (id: `bafyreigt3wmpnm2qduzijfubftw5ixrhqfrjrc2yi6hq2e4cpw6yer7hqq.25d1im923toai`)
- Skill custom dans `/home/node/.openclaw/workspace/skills/anytype/`
- Utiliser cet espace pour tout ce qui concerne Christophe et moi
- **Images** : ✅ ÇA MARCHE ! Anytype télécharge et internalise les images depuis une URL externe
- Méthode : inclure `![alt](http://192.168.1.150:3923/chemin/image.png)` dans le `body` markdown lors d'un **POST** (création d'objet)
- Anytype récupère l'image, lui donne un ID interne (`bafyrei...`) et la sert via `http://127.0.0.1:47800/image/<id>`
- L'image doit être accessible depuis le serveur Anytype (même réseau local)
- **PATCH** : utiliser le champ `"markdown"` (PAS `"body"`) pour modifier le contenu existant !
- Le format icon doit être `{"format":"emoji","name":"🧪"}` et non une string simple
- **POST** (création) : utiliser `"body"` pour le contenu
- **PATCH** (modification) : utiliser `"markdown"` pour le contenu
## CopyParty (stockage fichiers)
- URL : http://192.168.1.150:3923
- Upload simple via `curl -X PUT "http://192.168.1.150:3923/<chemin>" --data-binary @fichier`
- Pas d'authentification requise
- Dossier `/anytype/` créé pour les fichiers liés à Anytype
- Utilisable pour héberger images, schémas, etc. avec lien direct
- **Utile pour Penpot** : uploader une image sur CopyParty → l'injecter dans Penpot via `upload_file_media_from_url`
## Penpot MCP — Workflow & Gotchas
### Stack
- Penpot UI : http://192.168.1.150:9001
- MCP HTTP endpoint : http://192.168.1.150:9002/mcp (health: /health)
- MCP = zcube/penpot-mcp-server, image node:22-alpine + npm @zcubekr/penpot-mcp-server
- Token Penpot déjà configuré dans la stack Portainer (stack id=90)
### ⚠️ Règles critiques MCP zcube
**1. Tous les paramètres sont en camelCase** (PAS snake_case) :
- `projectId`, `fileId`, `pageId`, `parentId`
- `fillColor`, `fontSize`, `fontWeight`
- `isShared`, `gradientType`, `borderRadius`, `cornerRadius``r1/r2/r3/r4`
**2. Session unique — toujours redémarrer le container avant** :
- Le serveur n'autorise QU'UNE session à la fois
- `initialize` échoue avec "already initialized" si session existante
- Solution : inclure le restart Portainer dans le script (container `penpot-penpot-mcp-1`)
**3. Récupérer IDs depuis `get_profile`** (plus fiable que `list_teams`) :
- `profile.defaultProjectId` → projet par défaut (Drafts)
- `profile.defaultTeamId` → team
**4. `export_shape` NON IMPLÉMENTÉ** dans zcube — ne pas l'appeler
**5. `list_teams` retourne du texte** ("Found 1 teams"), pas du JSON
**6. `create_text` — paramètres corrects** :
- `text` (PAS `content`) → le contenu du texte
- `textAlign` (PAS `align`) → "left", "center", "right", "justify"
- `fontWeight` → string "100""900" ou "bold"
- `fillColor` → couleur HEX
- ⚠️ `parentId` **NON SUPPORTÉ** pour create_text — les textes vont toujours au root frame
- Width/height auto-calculés (text.length × fontSize × 0.6) — ne pas les spécifier
**7. Workflow création fichier** :
```
get_profile → defaultProjectId
create_file { projectId, name } → fileId
list_pages { fileId } → pageId (array[0].id)
create_frame { fileId, pageId, name, x, y, width, height, fillColor } → frameId
create_rectangle { fileId, pageId, parentId: frameId, ... } ← parentId OK pour rect
create_text { fileId, pageId, text, x, y, textAlign, ... } ← PAS de parentId
```
**8. Coordonnées** : absolues sur la page. Les textes se placent dans le root frame automatiquement. Pour un poster : mettre frame à (0,0) et textes aux bonnes coords absolues.
### Pour générer une image finale
- `export_shape` non dispo → utiliser **HTML/CSS → Playwright screenshot**
- Script template : `/home/node/.openclaw/workspace/screenshot-poster.mjs`
- Playwright : `node_modules/playwright`, chrome : `/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome`
- Sauvegarder dans workspace (pas /tmp) pour envoi Telegram
### Pour injecter une image dans Penpot
- Uploader sur CopyParty : `curl -X PUT http://192.168.1.150:3923/anytype/image.png --data-binary @image.png`
- Puis : `upload_file_media_from_url { fileId, url: "http://192.168.1.150:3923/anytype/image.png" }`
## Penpot API Directe — Pixel-perfect (⭐ À UTILISER EN PRIORITÉ)
### Pourquoi directe > MCP
- MCP : auto-calcule les dims texte, pas de `parentId` pour les textes → pas de contrôle pixel-perfect
- API directe : contrôle total (x, y, width, height, parentId, contenu riche)
### Accès
- **URL backend** : `http://192.168.1.150:9003/api/rpc/command/<cmd>` (port 9003 exposé = penpot-backend:6060)
- **Auth** : `Authorization: Token <access_token>` (dans l'en-tête HTTP)
- **Token** : récupérable dans Penpot UI → Profile → Access Tokens
- **Format corps** : JSON **kebab-case** (ex: `page-id`, `frame-id`, `fill-color`)
- **Réponse** : kebab-case aussi (`default-team-id`, `default-project-id`)
- ⚠️ L'accès via nginx port 9001 retourne un user vide même avec le bon token — **toujours utiliser le port 9003**
### Workflow création poster (Node.js)
```js
// 1. Auth + IDs
const prof = await rpc('get-profile');
const teamId = prof['default-team-id'];
const projectId = prof['default-project-id'];
// 2. Créer fichier
const file = await rpc('create-file', { 'project-id': projectId, name: 'Mon affiche', 'is-shared': false });
const FILE_ID = file.id;
const PAGE_ID = Object.keys(file.data?.['pages-index'] || {})[0] || file.data?.pages?.[0];
// 3. Ajouter shapes une par une (revn incrémental !)
for (const change of changes) {
const result = await rpc('update-file', {
id: FILE_ID, 'session-id': randomUUID(), revn: currentRevn, vern: 0,
changes: [change]
});
currentRevn = result.revn;
}
```
### Format d'une change `add-obj`
```js
{
type: 'add-obj',
id: '<uuid>',
'page-id': PAGE_ID,
'frame-id': ROOT, // '00000000-0000-0000-0000-000000000000' = root
'parent-id': ROOT,
obj: {
id, type: 'rect'|'text'|'frame', name,
x, y, width, height,
'parent-id': ROOT, 'frame-id': ROOT,
fills: [{ 'fill-color': '#C0392B', 'fill-opacity': 1 }],
selrect: { x, y, width, height, x1, y1, x2, y2 },
points: [{x,y},{x:x+w,y},{x:x+w,y:y+h},{x,y:y+h}],
transform: {a:1,b:0,c:0,d:1,e:0,f:0},
'transform-inverse': {a:1,b:0,c:0,d:1,e:0,f:0},
// Pour un text :
content: { type:'root', children:[{ type:'paragraph-set', children:[{
type:'paragraph', 'text-align':'center', children:[{
text: 'Mon texte',
'font-id':'gfont-work-sans', 'font-family':'Work Sans',
'font-size':'48', 'font-weight':'700', 'font-style':'normal',
'letter-spacing':'2', 'line-height': 1.2, 'text-decoration':'none',
fills:[{'fill-color':'#FFFFFF', 'fill-opacity':1}],
}]
}]}]}
}
}
```
### ⚠️ Gotchas critiques
- **Envoyer 1 change à la fois** (pas toutes en même temps) + incrémenter `revn`
- **`type: 'frame'`** échoue avec erreur `:shapes nil` → utiliser `type: 'rect'` comme fond
- **toKebab()** : tous les paramètres JS en camelCase → convertir en kebab-case avant envoi
- **`text` content** : bien mettre le champ `text:` dans le nœud enfant (sinon validation error)
- **Texte centré** : mettre `x:0, width:W` (toute la largeur) + `text-align: 'center'` dans le paragraphe
### Script de référence
`/home/node/.openclaw/workspace/penpot-api-direct.mjs` — poster 600×900 complet, 16 shapes
## Podcasts & Vidéos — Transcription
- Je peux **récupérer et transcrire** des podcasts/vidéos en ligne
- **Méthode :**
1. Utiliser **Playwright** pour intercepter les requêtes réseau et trouver l'URL du fichier audio (.mp3)
2. Télécharger le MP3 avec `curl -sL -A "Mozilla/5.0..." -H "Referer: <site>" <url> -o fichier.mp3`
3. Transcrire avec **Groq Whisper** : `curl -X POST https://api.groq.com/openai/v1/audio/transcriptions -H "Authorization: Bearer $GROQ_API_KEY" -F "file=@fichier.mp3" -F "model=whisper-large-v3-turbo" -F "language=fr" -F "response_format=text"`
- Testé avec succès sur BFM Business / Simplecast (podcasts hébergés sur simplecastaudio.com)
- Sauvegarder les fichiers dans le workspace, pas dans /tmp
- **YouTube** : utiliser **yt-dlp** (binaire dans `/home/node/.openclaw/workspace/yt-dlp`)
- Transcript via API YouTube (sous-titres auto) : `yt-dlp --write-auto-sub --skip-download --sub-lang fr -o workspace/transcript <url>`
- Audio pour Whisper : `yt-dlp -x --audio-format mp3 -o workspace/audio.mp3 <url>`
## Mémoire Vectorielle (Qdrant)
- **Collection** : `nox-memory` sur Qdrant (`http://192.168.1.150:6333`)
- **Stack Portainer** : `qdrant` (anciennement `kilocode-qdrant`, renommé 2026-02-22)
- **Script** : `/home/node/.openclaw/workspace/nox-memory.js`
- **Modèle** : `text-embedding-3-small` (OpenAI, 1536 dims, Cosine)
- **Accès** : `node fetch` natif (Node 22) — curl bloqué par sandbox OpenClaw, mais node fetch fonctionne ✅
- **Réseau Docker** : le stack doit avoir un réseau `bridge` en plus de `swag_lan` pour être accessible depuis le LXC
- **Usage** :
```bash
node nox-memory.js add "texte" --type fact|semantic|preference|episodic --tags "t1,t2" --importance 1-5
node nox-memory.js search "question naturelle" [--limit 5] [--type fact]
node nox-memory.js list [--type fact] [--limit 20]
node nox-memory.js stats
node nox-memory.js import-md MEMORY.md
```
- MEMORY.md importé (9 chunks, 2026-02-22)
- **Utiliser en priorité** pour les recherches contextuelles (memory_search reste utile pour le démarrage de session)
- ⚠️ Qdrant charge ~34 collections KiloCode au démarrage → ~30s avant d'être prêt
## Home Assistant — Lovelace / Dashboards
### Dashboards disponibles
- `lovelace` → "Aperçu" (dashboard principal, 15+ vues)
- `vue-par-pieces` → "Vue par pièces"
- `mushroom-strategy` → "Mushroom-strategy"
- `map` → Map
### Modifier/créer des vues Lovelace via WebSocket API
L'API REST `/api/lovelace/config` retourne 404 même si HA est en mode storage → **utiliser l'API WebSocket** à la place.
**Méthode (Node.js, WebSocket natif Node 22) :**
```js
const ws = new WebSocket(`${HA_URL.replace('http://','ws://')}/api/websocket`);
// Auth
ws.send(JSON.stringify({ type: 'auth', access_token: TOKEN }));
// Lire config
send({ type: 'lovelace/config' }) // → result.views[]
// Sauvegarder config modifiée
send({ type: 'lovelace/config/save', config: modifiedConfig })
// Lister les dashboards
send({ type: 'lovelace/dashboards/list' })
// Pour un dashboard spécifique (url_path)
send({ type: 'lovelace/config', url_path: 'vue-par-pieces' })
```
**Script template :** `/home/node/.openclaw/workspace/ha_ws_add_lights.mjs`
- Lire config → modifier le tableau `views` → sauvegarder
- Utiliser le WebSocket natif Node 22 (pas besoin du package `ws`)
- Timeout de sécurité à 15s
- Le code de sortie 1 (timeout) est normal si `ws.close()` ne coupe pas avant le timeout — le succès est dans le log
**Astuce :** Pour ajouter une vue sans écraser les existantes, lire d'abord la config, push la nouvelle vue, puis sauvegarder le tout.
## Portainer
- URL : https://192.168.1.150:9443
- Token : variable d'env `PORTAINER_API_KEY`
- Endpoint local : id=2 (socket Docker)
- ~38 stacks dont ~25 actives
- Pas de `jq` dans le conteneur → utiliser curl + Node.js directement
## Proxmox — Inventaire VMs/LXC clés
- **VM 138** (`mini-pc`) → **Home Assistant** (192.168.1.40) — pas d'agent QEMU
- **LXC 145** (`mini-pc`) → **hôte OpenClaw**
- ⚠️ **OpenClaw ne tourne pas directement dans le LXC** : il tourne dans un **conteneur Docker** à l'intérieur du **LXC 145**
- Pour les commandes `openclaw ...`, se placer dans le **conteneur Docker OpenClaw** (depuis le shell du LXC → `docker exec ...`)
- **LXC 139** (`mini-pc`) → **Frigate** (NVR)
- **VM 109** (`z820`) → **PBS** (Proxmox Backup Server, 192.168.1.91)
## Docker — Règles de mise à jour
- **Toujours exécuter `docker-backup.js` avant toute mise à jour d'un conteneur Docker**
- Script : `node /home/node/.openclaw/workspace/docker-backup.js <nom_conteneur>`
- Exemple : `node docker-backup.js vaultwarden` avant de puller une nouvelle image
- Services configurés : vaultwarden, vikunja, nocodb, freshrss
- Le cron hebdo a été créé mais **désactivé** à la demande de Christophe (2026-02-23)
- Réactiver si besoin : `node /app/dist/index.js cron add ...`
## Santé — Christophe
- **Profil :** Homme, né 12/06/1981 (44 ans), 1m81, ~90 kg, IMC ~27,5
- **Appareil tension :** Sanitas (poignet, profil 1)
- **Suivi tension** dans `memory/YYYY-MM-DD.md` — tableau SYS/DIA/Pouls
- **Première mesure :** 2026-02-25 12:08 → 145/94 mmHg, pouls 77 bpm (HTA stade 1)
## VidBee — Téléchargement vidéo
- UI : http://192.168.1.150:3800 | API : http://192.168.1.150:3801
- Stack Portainer : `vidbee` (id=101)
- Fichiers dans : `/share/ZFS24_DATA/docker/vidbee/downloads/`
- **Toutes les routes API sont en POST sous `/rpc/<route>`** (pas `/openapi/`, pas sans préfixe)
- **Format oRPC** : body `{"json": {...}}`, réponse `{"json": {...}}`
- Routes clés :
- `POST /rpc/videoInfo {"json":{"url":"..."}}` → infos + liste des formats
- `POST /rpc/downloads/create {"json":{"url":"...","type":"video","title":"...","selectedFormat":{...}}}` → lance le DL
- `POST /rpc/downloads/list {"json":{}}` → téléchargements en cours
- `POST /rpc/history/list {"json":{}}` → historique (completed, etc.)
- ⚠️ **NE PAS utiliser `selectedFormat`** — ne gère pas le merge ffmpeg → donne du 360p
- **Utiliser `format`** avec la string yt-dlp : `"format":"299+140"` (video_id + audio_id)
- Workflow correct :
1. `POST /rpc/videoInfo` → récupérer les format IDs
2. Prendre le meilleur format vidéo-only 1080p mp4 (ex: `299`) + audio m4a (ex: `140`)
3. `POST /rpc/downloads/create {"json":{"url":"...","type":"video","format":"299+140"}}`
- Si le fichier existe déjà → le supprimer via exec dans le container (`rm /data/downloads/*.mp4`) avant de relancer
## Sites & Flux RSS
- **Korben.info** : utiliser le flux RSS `https://korben.info/feed` pour récupérer les articles (plus propre que scraper la homepage)
## Veilles actives
### 📡 XGS-PON CMTD — Montlieu-la-Garde
- Christophe est sur le réseau **CMTD** (RIP, géré par Orange) actuellement en GPON → plafond ~1 Gbps
- Migration XGS-PON en cours, prévue fin 2026
- **Quand XGS-PON dispo** → recommander passage **Freebox Ultra grand public** (moins cher que FreePro, mêmes features + 8 Gbps sym)
- Vérifier 1x/mois via heartbeat
## Leçons apprises
- **Toujours prendre un snapshot du LXC 145** (`mini-pc`) avant toute modification de config ou installation qui pourrait casser OpenClaw
- Commande : `curl -sk -X POST -H "Authorization: PVEAPIToken=$PVE_TOKEN" -H "Content-Type: application/json" -d '{"snapname":"<nom>","description":"<desc>"}' "$PVE_URL/api2/json/nodes/mini-pc/lxc/145/snapshot"`
- Toujours vérifier les `friendly_name` dans HA pour trouver les entités
- Les skills custom sont dans `/home/node/.openclaw/workspace/skills/`
- **curl est bloqué** dans le sandbox OpenClaw pour certaines destinations réseau, mais **node fetch (Node 22)** fonctionne — utiliser node pour les requêtes HTTP internes quand curl échoue