From 7ecc2abe0a09b5560c1cc2898c35e81fda7e11ad Mon Sep 17 00:00:00 2001 From: root Date: Fri, 12 Jun 2026 18:36:31 +0000 Subject: [PATCH] =?UTF-8?q?feat(agent):=20am=C3=A9lioration=20UI=20=C3=A9t?= =?UTF-8?q?udiante=20-=20instances,=20statut=20et=20lien=20site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- agent/instance.go | 123 ++++++++++++++++++++++++++++++++++++++++++++ agent/ui.go | 27 ++++++---- agent/ui/index.html | 121 +++++++++++++++++++++++++++++++++---------- agent/websocket.go | 24 +++++++++ 4 files changed, 259 insertions(+), 36 deletions(-) create mode 100644 agent/instance.go diff --git a/agent/instance.go b/agent/instance.go new file mode 100644 index 0000000..be217d8 --- /dev/null +++ b/agent/instance.go @@ -0,0 +1,123 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +type InstanceInfo struct { + ID string `json:"id"` + TemplateName string `json:"templateName,omitempty"` + Port int `json:"port"` + Status string `json:"status"` +} + +func instancesFile(dataDir string) string { + return filepath.Join(dataDir, "instances.json") +} + +func loadInstances(dataDir string) (map[string]*InstanceInfo, error) { + f := instancesFile(dataDir) + data, err := os.ReadFile(f) + if err != nil { + if os.IsNotExist(err) { + return make(map[string]*InstanceInfo), nil + } + return nil, err + } + var list []*InstanceInfo + if err := json.Unmarshal(data, &list); err != nil { + return nil, err + } + m := make(map[string]*InstanceInfo) + for _, inst := range list { + m[inst.ID] = inst + } + return m, nil +} + +func saveInstances(dataDir string, instances map[string]*InstanceInfo) error { + if err := os.MkdirAll(dataDir, 0755); err != nil { + return err + } + list := make([]*InstanceInfo, 0, len(instances)) + for _, inst := range instances { + list = append(list, inst) + } + data, err := json.MarshalIndent(list, "", " ") + if err != nil { + return err + } + return os.WriteFile(instancesFile(dataDir), data, 0644) +} + +func upsertInstance(dataDir string, inst *InstanceInfo) error { + instances, err := loadInstances(dataDir) + if err != nil { + return err + } + instances[inst.ID] = inst + return saveInstances(dataDir, instances) +} + +func removeInstance(dataDir, instanceID string) error { + instances, err := loadInstances(dataDir) + if err != nil { + return err + } + delete(instances, instanceID) + return saveInstances(dataDir, instances) +} + +func instanceURL(inst *InstanceInfo) string { + return fmt.Sprintf("http://localhost:%d", inst.Port) +} + +func getInstanceStatus(dataDir, instanceID string) string { + dir := instanceDir(dataDir, instanceID) + composeFile := filepath.Join(dir, "docker-compose.yml") + if _, err := os.Stat(composeFile); err != nil { + return "stopped" + } + + engine := getContainerEngine() + cmd := exec.Command(engine, "compose", "-f", composeFile, "ps", "--format", "json") + out, err := cmd.Output() + if err != nil { + return "error" + } + + outStr := strings.TrimSpace(string(out)) + if outStr == "" || outStr == "[]" { + return "stopped" + } + + // Docker compose JSON output can be a single object or an array + if strings.HasPrefix(outStr, "[") { + var containers []map[string]interface{} + if err := json.Unmarshal(out, &containers); err != nil { + return "error" + } + for _, c := range containers { + state, _ := c["State"].(string) + if state == "running" { + return "running" + } + } + return "stopped" + } + + var c map[string]interface{} + if err := json.Unmarshal(out, &c); err != nil { + return "error" + } + state, _ := c["State"].(string) + if state == "running" { + return "running" + } + return "stopped" +} diff --git a/agent/ui.go b/agent/ui.go index d27f8cc..e97cce6 100644 --- a/agent/ui.go +++ b/agent/ui.go @@ -5,8 +5,6 @@ import ( "fmt" "log" "net/http" - "os" - "path/filepath" "github.com/gorilla/websocket" ) @@ -81,17 +79,28 @@ func startUI(dataDir, nodeID, serverAddr string) { } func listInstances(dataDir string, conn *websocket.Conn) { - dir := filepath.Join(dataDir, "instances") - entries, err := os.ReadDir(dir) + instances, err := loadInstances(dataDir) if err != nil { + log.Printf("loadInstances error: %v", err) conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": []interface{}{}}) return } - var instances []map[string]interface{} - for _, e := range entries { - if e.IsDir() { - instances = append(instances, map[string]interface{}{"id": e.Name()}) + + var list []map[string]interface{} + for _, inst := range instances { + status := getInstanceStatus(dataDir, inst.ID) + if status != inst.Status { + inst.Status = status + _ = upsertInstance(dataDir, inst) } + list = append(list, map[string]interface{}{ + "id": inst.ID, + "templateName": inst.TemplateName, + "port": inst.Port, + "status": inst.Status, + "url": instanceURL(inst), + }) } - conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": instances}) + + conn.WriteJSON(map[string]interface{}{"action": "instances_list", "instances": list}) } diff --git a/agent/ui/index.html b/agent/ui/index.html index 30cff67..e72b720 100644 --- a/agent/ui/index.html +++ b/agent/ui/index.html @@ -3,32 +3,54 @@ EduBox Agent - -
-

EduBox Agent

-
-

Chargement...

+
+
+

EduBox Agent

+
+

Connexion en cours...

+
+
+
diff --git a/agent/websocket.go b/agent/websocket.go index c38bdef..05e5eda 100644 --- a/agent/websocket.go +++ b/agent/websocket.go @@ -172,35 +172,59 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) }) case "start": log.Printf("Start instance %s on port %d", msg.InstanceID, msg.Port) + if err := upsertInstance(dataDir, &InstanceInfo{ + ID: msg.InstanceID, + TemplateName: msg.Type, + Port: msg.Port, + Status: "starting", + }); err != nil { + log.Printf("upsertInstance error: %v", err) + } if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil { log.Printf("writeCompose error: %v", err) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"}) sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil { log.Printf("dockerComposeUp error: %v", err) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"}) sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } + status := getInstanceStatus(dataDir, msg.InstanceID) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status}) sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port}) + notifyUI(map[string]interface{}{"action": "instances_updated"}) case "stop": log.Printf("Stop instance %s", msg.InstanceID) if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil { log.Printf("dockerComposeDown error: %v", err) } + if inst, _ := loadInstances(dataDir); inst[msg.InstanceID] != nil { + inst[msg.InstanceID].Status = "stopped" + _ = saveInstances(dataDir, inst) + } + notifyUI(map[string]interface{}{"action": "instances_updated"}) case "reset": log.Printf("Reset instance %s", msg.InstanceID) dockerComposeRm(dataDir, msg.InstanceID) if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil { log.Printf("writeCompose error: %v", err) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"}) + sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil { log.Printf("dockerComposeUp error: %v", err) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: "error"}) sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()}) return } + status := getInstanceStatus(dataDir, msg.InstanceID) + _ = upsertInstance(dataDir, &InstanceInfo{ID: msg.InstanceID, TemplateName: msg.Type, Port: msg.Port, Status: status}) sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port}) + notifyUI(map[string]interface{}{"action": "instances_updated"}) default: log.Printf("Unknown action: %s", msg.Action) }