@@ -277,6 +277,76 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
277277 }
278278 } ) ;
279279
280+ // Background auto-rediscovery worker — refreshes metadata for all previously-discovered
281+ // containers every 60 s. This ensures that after a `docker compose up --force-recreate`
282+ // (or any redeploy that recreates containers), the dashboard picks up the new container_id
283+ // and freshly-built log/start/stop commands automatically, without requiring a manual
284+ // "Discover" click or an app restart.
285+ let rediscover_state = state. clone ( ) ;
286+ tokio:: spawn ( async move {
287+ let mut interval = tokio:: time:: interval ( tokio:: time:: Duration :: from_secs ( 60 ) ) ;
288+ loop {
289+ interval. tick ( ) . await ;
290+
291+ // Collect unique (host, ssh_user) pairs that have at least one discovered service.
292+ let hosts: Vec < ( String , Option < String > ) > = {
293+ let s = rediscover_state. services . read ( ) . await ;
294+ let mut seen = std:: collections:: HashSet :: new ( ) ;
295+ let mut result = Vec :: new ( ) ;
296+ for svc in s. iter ( ) . filter ( |x| x. discovered ) {
297+ let key = ( svc. host . clone ( ) , svc. ssh_user . clone ( ) ) ;
298+ if seen. insert ( key. clone ( ) ) {
299+ result. push ( key) ;
300+ }
301+ }
302+ result
303+ } ;
304+
305+ for ( host, ssh_user) in hosts {
306+ // Only skip YAML-configured services; rediscover all previously-discovered ones.
307+ let static_ids: std:: collections:: HashSet < String > = {
308+ let s = rediscover_state. services . read ( ) . await ;
309+ s. iter ( ) . filter ( |x| !x. discovered ) . map ( |x| x. id . clone ( ) ) . collect ( )
310+ } ;
311+
312+ match discover:: discover_docker_services ( & host, ssh_user. as_deref ( ) , & static_ids) . await {
313+ Ok ( found) => {
314+ let mut changed = false ;
315+ {
316+ let mut s = rediscover_state. services . write ( ) . await ;
317+ for svc in found {
318+ if let Some ( existing) = s. iter_mut ( ) . find ( |x| x. id == svc. id && x. discovered ) {
319+ // Only overwrite (and flag as changed) if something meaningful differs.
320+ if existing. container_id != svc. container_id || existing. status != svc. status {
321+ * existing = svc;
322+ changed = true ;
323+ }
324+ } else if !s. iter ( ) . any ( |x| x. id == svc. id ) {
325+ s. push ( svc) ;
326+ changed = true ;
327+ }
328+ }
329+ }
330+ if changed {
331+ let s = rediscover_state. services . read ( ) . await ;
332+ let snapshot = json ! ( { "type" : "full_state" , "services" : & * s} ) . to_string ( ) ;
333+ let _ = rediscover_state. broadcaster . send ( snapshot) ;
334+ let host_services: Vec < _ > = s
335+ . iter ( )
336+ . filter ( |svc| svc. host == host && svc. discovered )
337+ . cloned ( )
338+ . collect ( ) ;
339+ config:: save_discovered_services ( & host, & host_services) ;
340+ }
341+ }
342+ Err ( e) => {
343+ tracing:: warn!( "auto-rediscovery failed for {}: {}" , host, e) ;
344+ }
345+ }
346+ }
347+ }
348+ } ) ;
349+
280350 // Plugin directory (can be overridden with PLUGIN_DIR env var).
281351 // When deployed, plugins live in bin/ next to the executable.
282352 // When running from source (cargo run), fall back to plugins/bin relative to the workspace root.
@@ -552,23 +622,35 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
552622 Err ( _) => return axum:: Json ( json ! ( { "ok" : false , "error" : "invalid json — expected {host, ssh_user}" } ) ) ,
553623 } ;
554624
555- // Collect existing service ids so we don't duplicate
556- let existing_ids: std:: collections:: HashSet < String > = {
625+ // Only skip YAML-configured (non-discovered) services to avoid overriding user
626+ // config. Previously-discovered services are always refreshed so that redeployed
627+ // containers (same name, new container ID) get up-to-date metadata — container_id,
628+ // log_cmd, start/stop/restart commands — without requiring an app restart.
629+ let static_ids: std:: collections:: HashSet < String > = {
557630 let s = state. services . read ( ) . await ;
558- s. iter ( ) . map ( |x| x. id . clone ( ) ) . collect ( )
631+ s. iter ( ) . filter ( |x| !x . discovered ) . map ( |x| x. id . clone ( ) ) . collect ( )
559632 } ;
560633
561634 match discover:: discover_docker_services (
562635 & body. host ,
563636 body. ssh_user . as_deref ( ) ,
564- & existing_ids ,
637+ & static_ids ,
565638 ) . await {
566- Ok ( new_services ) => {
567- let count = new_services . len ( ) ;
568- // Merge into state
639+ Ok ( found_services ) => {
640+ let mut new_count = 0usize ;
641+ let mut updated_count = 0usize ;
569642 {
570643 let mut s = state. services . write ( ) . await ;
571- s. extend ( new_services. clone ( ) ) ;
644+ for svc in found_services. clone ( ) {
645+ if let Some ( existing) = s. iter_mut ( ) . find ( |x| x. id == svc. id && x. discovered ) {
646+ // Refresh the existing discovered service with up-to-date metadata
647+ * existing = svc;
648+ updated_count += 1 ;
649+ } else if !s. iter ( ) . any ( |x| x. id == svc. id ) {
650+ s. push ( svc) ;
651+ new_count += 1 ;
652+ }
653+ }
572654 }
573655 // Broadcast updated full state
574656 {
@@ -586,7 +668,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
586668 . collect ( ) ;
587669 config:: save_discovered_services ( & body. host , & host_services) ;
588670 }
589- axum:: Json ( json ! ( { "ok" : true , "discovered" : count, "services" : new_services} ) )
671+ axum:: Json ( json ! ( {
672+ "ok" : true ,
673+ "discovered" : new_count,
674+ "updated" : updated_count,
675+ "services" : found_services
676+ } ) )
590677 }
591678 Err ( e) => axum:: Json ( json ! ( { "ok" : false , "error" : format!( "{}" , e) } ) ) ,
592679 }
0 commit comments