# ntfy-bridge — Architecture ## Philosophie ntfy-bridge est un **daemon HTTP léger** écrit en V (~500 lignes) qui compile en un seul binaire natif statique. Il écoute des webhooks, se connecte au socket Docker, et poll des endpoints HTTP — puis transforme chaque événement en notification Ntfy formatée. **Principes :** - **Un binaire, zéro runtime** — pas de Node, Python, JVM ou conteneur obligatoire - **Config-driven** — tout passe par le fichier YAML, pas de recompilation - **Fail-safe** — un échec réseau vers Ntfy ne crashe pas le daemon - **Silencieux par défaut** — ne spamme pas, utilise les bons niveaux de priorité Ntfy (1-5) - **Extensible** — ajouter une source = implémenter un module dans `sources/` ## Architecture globale ``` ┌─────────────────────────────────────────────────────────────┐ │ ntfy-bridge │ │ │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────────┐ │ │ │ HTTP │ │ Docker │ │ HTTP │ │ Cron │ │ │ │ Server │ │ Watcher │ │ Poller │ │ Receiver │ │ │ │ :9090 │ │ goroutine│ │ goroutine│ │ /webhooks │ │ │ └────┬─────┘ └────┬─────┘ └────┬─────┘ └─────┬──────┘ │ │ │ │ │ │ │ │ └──────────────┼──────────────┼──────────────┘ │ │ ▼ ▼ │ │ ┌──────────────────────────┐ │ │ │ Event Pipeline │ │ │ │ ┌────────┐ ┌────────┐ │ │ │ │ │Parse & │ │Dedup & │ │ │ │ │ │Format │──│RateLim │ │ │ │ │ └────────┘ └────────┘ │ │ │ └────────────┬─────────────┘ │ │ ▼ │ │ ┌──────────────────────────┐ │ │ │ Ntfy Publisher │ │ │ │ POST / │ │ │ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ## Flux de données ### 1. Webhook entrant (Gitea, Uptime Kuma, generic) ``` POST /webhooks/gitea-flowdeck │ ▼ 1. Verify signature (optionnel) 2. Parse JSON → Event struct 3. Route vers le transformer approprié 4. Appliquer priority_map 5. Appliquer template de message 6. POST vers Ntfy ``` ### 2. Docker watcher (socket) ``` goroutine DockerWatcher │ ▼ 1. Connexion au socket Docker (/var/run/docker.sock) 2. Stream events via GET /events?filters=... 3. Filtrer par événements configurés (die, oom, health_status) 4. Extraire container_name, image, exit_code 5. Appliquer template 6. POST vers Ntfy ``` ### 3. HTTP Poller ``` goroutine HttpPoller (toutes les N secondes) │ ▼ 1. GET avec timeout configuré 2. Si status ≠ expected → Event(priority=5) 3. Si OK et précédemment DOWN → Event(priority=1, tags="white_check_mark") 4. Sinon → rien (pas de notification si tout va bien) ``` ### 4. Cron receiver ``` POST /webhooks/cron-disk │ ▼ 1. Body = texte brut du script cron 2. Pass-through vers Ntfy 3. Le script cron contrôle son propre format ``` ## Modèle de données ```v struct Config { // dans config.v server ServerConfig defaults DefaultConfig sources SourcesConfig } struct ServerConfig { url string // URL du serveur Ntfy auth_token string // Token d'auth (optionnel) listen string // Adresse d'écoute HTTP (:9090) } struct DefaultConfig { priority int // 1-5 tags []string // ["loudspeaker"] } struct Source { // types spécifiques dans sources/event.v name string webhook_path string topic string template string // Go-like template priority_map map[string]int tags []string } struct Event { // dans sources/event.v source string // "gitea", "docker", "uptime_kuma", "http_poll", "cron" name string // nom de la source configurée topic string // topic Ntfy cible priority int // 1-5 tags []string message string // message formaté final raw string // payload brut (pour debug) } ``` ## Pipeline de transformation Chaque source implémente l'interface : ```v interface Transformer { transform(raw string, source &Source) ?Event } ``` ### Transformers inclus | Transformer | Entrée | Sortie | |---|---|---| | `gitea_transformer` | JSON webhook Gitea | `🔨 [FlowDeck] bruno pushed to main: "Fix bug" (a3f2c1d)` | | `uptime_kuma_transformer` | JSON webhook Kuma | `🚨 Gitea is DOWN — 503 — since 14:32` | | `docker_transformer` | Docker event JSON | `🐳 flowdeck exited OOMKilled on docker-prod-1` | | `http_poll_transformer` | HTTP response | `❌ obsigate.dracodev.net/health → timeout 30s` | | `generic_transformer` | Raw body | Pass-through, pas de transformation | ### Priorités Ntfy | Niveau | Usage | Exemple | |---|---|---| | 5 (urgent) | Service critique down, container OOM | Uptime Kuma DOWN | | 4 (high) | PR ouverte, container crash | Gitea pull_request, Docker die | | 3 (default) | Push, issue, activité normale | Gitea push | | 2 (low) | Info, succès | Service back UP | | 1 (min) | Debug, heartbeat | Health check OK (si configuré) | ## Déduplication et rate limiting - **Déduplication** : même source + même type d'événement + même cible → cooldown 5 minutes - **Rate limiting** : max 10 notifs/minute/topic (configurable) - **Uptime Kuma spécifique** : les "down" répétés dans un intervalle de 2 min sont dédupliqués ## Structure du projet ``` ntfy-bridge/ ├── README.md # Vue d'ensemble, quickstart ├── ARCHITECTURE.md # Ce document ├── ROADMAP.md # Phases de développement ├── CONTRIBUTING.md # Guide de contribution ├── ntfy-bridge.example.yaml # Exemple de configuration complet ├── main.v # Entrypoint, parsing CLI ├── config.v # Chargement + validation YAML ├── server.v # HTTP server + routing webhooks ├── ntfy.v # Client HTTP Ntfy (POST) ├── dedup.v # Déduplication + rate limiting ├── template.v # Mini moteur de template ├── log.v # Logging structuré ├── docker_watcher.v # Watcher socket Docker (Unix) ├── sources/ # Types Event + transformers │ ├── event.v # Types Event, Source │ ├── gitea.v # Transformer Gitea │ ├── uptime_kuma.v # Transformer Uptime Kuma │ ├── docker.v # Transformer Docker events │ ├── http_poll.v # Transformer HTTP poll │ ├── generic.v # Webhook passe-partout │ └── cron.v # Webhook cron ├── *_test.v # Tests unitaires ├── scripts/ │ └── example-cron-disk.sh # Exemple script cron → Ntfy └── v.mod # Dépendances V ``` ## Dépendances V (v.mod) ```v Module { name: 'ntfy-bridge' description: 'Homelab notification hub — aggregates Gitea, Docker, Uptime Kuma → Ntfy' version: '0.1.0' deps: [ 'ui-driver.cua' ] } ``` Dépendances externes minimales : - `vsl` — parsing YAML (sinon implémenter un parseur minimal) - `x.net` — client HTTP (ou lib standard V `net`) - `json` — stdlib V pour le parsing JSON En réalité, V a tout dans sa stdlib : `net.http`, `json`, `time`, `os`, `flag`. Le projet vise **zéro dépendance externe**. ## Déploiement ### Option A : Binaire nu + systemd ```ini # /etc/systemd/system/ntfy-bridge.service [Unit] Description=ntfy-bridge notification hub After=network.target [Service] ExecStart=/usr/local/bin/ntfy-bridge --config /etc/ntfy-bridge.yaml Restart=always RestartSec=10 [Install] WantedBy=multi-user.target ``` ### Option B : Conteneur Docker ```dockerfile FROM alpine:latest COPY ntfy-bridge /usr/local/bin/ COPY ntfy-bridge.yaml /etc/ EXPOSE 9090 CMD ["/usr/local/bin/ntfy-bridge", "--config", "/etc/ntfy-bridge.yaml"] ``` ### Option C : Compilation croisée ```bash v -os windows . -o ntfy-bridge.exe # Pour Windows v -os linux . -o ntfy-bridge # Pour Linux v -os macos . -o ntfy-bridge-mac # Pour macOS ``` ## Sécurité - **Signature webhooks** : vérification HMAC-SHA256 optionnelle pour Gitea - **Auth token** : token optionnel pour le serveur Ntfy (si auth activée) - **Pas d'exposition externe** : le serveur HTTP écoute sur localhost par défaut (:9090) - **Rate limiting interne** : max 10 notifs/minute/source pour éviter le spam - **Input validation** : tout JSON entrant est validé avant processing - **No secrets in config** : le token Ntfy peut être passé via variable d'environnement `NTFY_TOKEN` ## Métriques et observabilité - **Health endpoint** : `GET /health` → 200 OK + uptime + compteur de notifs - **Stats endpoint** : `GET /stats` → compteurs par source, erreurs, latence Ntfy - **Logging** : stdout en JSON structuré (timestamp, level, source, event, error)