feat: add port clickable

This commit is contained in:
2026-05-19 16:58:44 +02:00
parent d1db386e1d
commit b3176c4dfa
5 changed files with 62 additions and 17 deletions

View File

@ -2,9 +2,12 @@
Interface web pour gérer les containers Docker de plusieurs machines depuis un seul endroit. Interface web pour gérer les containers Docker de plusieurs machines depuis un seul endroit.
- Visualisation en temps réel de tous les containers par host - Visualisation en temps réel de tous les containers, groupés par host et par projet compose
- Actions : start, stop, restart, remove - Actions : start, stop, restart sur un container ou sur tout un projet compose
- Streaming de logs en direct - Streaming de logs en direct (par container ou pour tout un projet simultanément)
- Auto-update automatique et manuel des images Docker
- Gestion des volumes, images et réseaux
- Liens directs vers les services exposés (clic sur un port)
- Gestion des agents depuis l'interface admin - Gestion des agents depuis l'interface admin
- PWA installable sur mobile - PWA installable sur mobile
@ -71,8 +74,11 @@ curl -fsSL https://gitea.anthonybouteiller.ovh/blomios/Containarr/raw/branch/mai
environment: environment:
CONTAINARR_SERVER_URL: "http://<ip-du-serveur>:9090" CONTAINARR_SERVER_URL: "http://<ip-du-serveur>:9090"
CONTAINARR_AGENT_TOKEN: "<token-copié-depuis-l-admin>" CONTAINARR_AGENT_TOKEN: "<token-copié-depuis-l-admin>"
CONTAINARR_HOST_IP: "<ip-lan-de-la-vm>" # ex: 192.168.1.95 — utilisé pour les liens de ports
``` ```
> `CONTAINARR_HOST_IP` est optionnel mais recommandé : sans lui, l'IP affichée sera l'adresse réseau Docker interne, et les liens vers les services exposés ne fonctionneront pas correctement.
### 4. Lancer ### 4. Lancer
```bash ```bash
@ -83,6 +89,18 @@ L'agent apparaît dans l'interface dans les secondes qui suivent.
--- ---
## Auto-update
Containarr peut surveiller et mettre à jour automatiquement les images Docker de vos containers.
- **Par container** : clic sur l'icône ↻ d'un container → activer l'auto-update et choisir l'intervalle de vérification
- **Par projet** : même bouton au niveau du groupe projet → applique la policy à tous les containers du projet
- **Mise à jour manuelle** : bouton "Mettre à jour maintenant" dans le panneau auto-update
Les mises à jour utilisent l'API Docker directement (pull de la nouvelle image + recréation du container avec la même configuration).
---
## Ports ## Ports
| Port | Usage | | Port | Usage |

View File

@ -69,6 +69,7 @@ async fn run(url: &str, token: &str, hostname: &str, docker: DockerClient) -> Re
hostname: hostname.to_string(), hostname: hostname.to_string(),
arch: std::env::consts::ARCH.to_string(), arch: std::env::consts::ARCH.to_string(),
os: std::env::consts::OS.to_string(), os: std::env::consts::OS.to_string(),
ip_address: std::env::var("CONTAINARR_HOST_IP").unwrap_or_default(),
})), })),
}) })
.await?; .await?;
@ -845,9 +846,11 @@ mod tests {
hostname: "host".to_string(), hostname: "host".to_string(),
arch: "x86_64".to_string(), arch: "x86_64".to_string(),
os: "linux".to_string(), os: "linux".to_string(),
ip_address: "192.168.1.10".to_string(),
}; };
assert_eq!(hs.token, "tok"); assert_eq!(hs.token, "tok");
assert_eq!(hs.hostname, "host"); assert_eq!(hs.hostname, "host");
assert_eq!(hs.ip_address, "192.168.1.10");
} }
#[test] #[test]
@ -858,6 +861,7 @@ mod tests {
hostname: "h".to_string(), hostname: "h".to_string(),
arch: "arm64".to_string(), arch: "arm64".to_string(),
os: "linux".to_string(), os: "linux".to_string(),
ip_address: String::new(),
})), })),
}; };
assert!(matches!( assert!(matches!(

View File

@ -28,10 +28,11 @@ message ContainerInfo {
// ── Agent → Server ──────────────────────────────────────────────────────────── // ── Agent → Server ────────────────────────────────────────────────────────────
message AgentHandshake { message AgentHandshake {
string token = 1; string token = 1;
string hostname = 2; string hostname = 2;
string arch = 3; string arch = 3;
string os = 4; string os = 4;
string ip_address = 5;
} }
message ImageInfo { message ImageInfo {

View File

@ -53,6 +53,10 @@ func (g *Gateway) Tunnel(stream agentv1.AgentGateway_TunnelServer) error {
ipAddress = host ipAddress = host
} }
} }
// If the agent advertises its own LAN IP, prefer it over the peer address.
if hs.IpAddress != "" {
ipAddress = hs.IpAddress
}
agentID := existing.ID agentID := existing.ID
slog.Info("agent connected", "id", agentID, "hostname", hs.Hostname, "ip", ipAddress) slog.Info("agent connected", "id", agentID, "hostname", hs.Hostname, "ip", ipAddress)

View File

@ -898,9 +898,14 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each uniquePorts(container.ports) as port} {#each uniquePorts(container.ports) as port}
<span class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20"> <a
href="http://{byAgent[agent_id]?.[0]?.ip_address ?? ''}:{port.host_port}"
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20 hover:bg-signal-cyan/25 hover:border-signal-cyan/40 transition-colors cursor-pointer"
>
{port.host_port}:{port.container_port} {port.host_port}:{port.container_port}
</span> </a>
{/each} {/each}
</div> </div>
</td> </td>
@ -941,10 +946,14 @@
{#if uniquePorts(container.ports).length > 0} {#if uniquePorts(container.ports).length > 0}
<div class="flex flex-wrap gap-1 mb-3"> <div class="flex flex-wrap gap-1 mb-3">
{#each uniquePorts(container.ports) as port} {#each uniquePorts(container.ports) as port}
<span class="font-mono text-xs px-1.5 py-0.5 rounded <a
bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20"> href="http://{byAgent[agent_id]?.[0]?.ip_address ?? ''}:{port.host_port}"
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20 hover:bg-signal-cyan/25 hover:border-signal-cyan/40 transition-colors cursor-pointer"
>
{port.host_port}:{port.container_port} {port.host_port}:{port.container_port}
</span> </a>
{/each} {/each}
</div> </div>
{/if} {/if}
@ -1005,9 +1014,14 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
<div class="flex flex-wrap gap-1"> <div class="flex flex-wrap gap-1">
{#each uniquePorts(container.ports) as port} {#each uniquePorts(container.ports) as port}
<span class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20"> <a
href="http://{byAgent[agent_id]?.[0]?.ip_address ?? ''}:{port.host_port}"
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20 hover:bg-signal-cyan/25 hover:border-signal-cyan/40 transition-colors cursor-pointer"
>
{port.host_port}:{port.container_port} {port.host_port}:{port.container_port}
</span> </a>
{/each} {/each}
</div> </div>
</td> </td>
@ -1048,10 +1062,14 @@
{#if uniquePorts(container.ports).length > 0} {#if uniquePorts(container.ports).length > 0}
<div class="flex flex-wrap gap-1 mb-3"> <div class="flex flex-wrap gap-1 mb-3">
{#each uniquePorts(container.ports) as port} {#each uniquePorts(container.ports) as port}
<span class="font-mono text-xs px-1.5 py-0.5 rounded <a
bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20"> href="http://{byAgent[agent_id]?.[0]?.ip_address ?? ''}:{port.host_port}"
target="_blank"
rel="noopener noreferrer"
class="font-mono text-xs px-1.5 py-0.5 rounded bg-signal-cyan/10 text-signal-cyan border border-signal-cyan/20 hover:bg-signal-cyan/25 hover:border-signal-cyan/40 transition-colors cursor-pointer"
>
{port.host_port}:{port.container_port} {port.host_port}:{port.container_port}
</span> </a>
{/each} {/each}
</div> </div>
{/if} {/if}