fix: activation via connexion WS principale, cascade delete student→nodes, lien fiche étudiant
- agent/websocket.go: expose sendMessage() + notifyUI() pour broadcaster les résultats d'activation à tous les clients UI connectés - agent/ui.go: supprime forwardActivation(), utilise sendMessage() sur la connexion WS principale au lieu d'une connexion temporaire - agent/activation.go: ajoute os.MkdirAll avant l'écriture d'activation.json - server/prisma/schema.prisma: onDelete Cascade sur Node→Student - server/app/dashboard/students/page.tsx: nom cliquable vers fiche détail - server/app/dashboard/students/[id]/actions.ts: deleteMany → delete
This commit is contained in:
@@ -29,6 +29,9 @@ func loadActivation(dataDir string) (*Activation, error) {
|
||||
}
|
||||
|
||||
func saveActivation(dataDir string, a *Activation) error {
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
f := activationFile(dataDir)
|
||||
data, err := json.MarshalIndent(a, "", " ")
|
||||
if err != nil {
|
||||
|
||||
+21
-28
@@ -29,13 +29,27 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
log.Printf("UI client connected from %s", r.RemoteAddr)
|
||||
|
||||
// Register notifier to forward activation results from main WS to this UI connection
|
||||
notifierID := registerUINotifier(func(msg map[string]interface{}) {
|
||||
log.Printf("UI notifier forwarding to browser: %+v", msg)
|
||||
if err := conn.WriteJSON(msg); err != nil {
|
||||
log.Printf("UI notify error: %v", err)
|
||||
} else {
|
||||
log.Printf("UI notifier sent successfully")
|
||||
}
|
||||
})
|
||||
defer unregisterUINotifier(notifierID)
|
||||
|
||||
for {
|
||||
var msg map[string]interface{}
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
log.Printf("UI client disconnected: %v", err)
|
||||
break
|
||||
}
|
||||
action, _ := msg["action"].(string)
|
||||
log.Printf("UI received action: %s", action)
|
||||
switch action {
|
||||
case "check":
|
||||
act, err := loadActivation(dataDir)
|
||||
@@ -46,10 +60,13 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
}
|
||||
case "activate":
|
||||
code, _ := msg["code"].(string)
|
||||
// Forward to server WS
|
||||
go func() {
|
||||
forwardActivation(serverAddr, nodeID, code, conn)
|
||||
}()
|
||||
log.Printf("UI handling activate with code: %s", code)
|
||||
if err := sendMessage(WSMessage{Action: "activate", NodeID: nodeID, Code: code}); err != nil {
|
||||
log.Printf("UI sendMessage failed: %v", err)
|
||||
conn.WriteJSON(map[string]interface{}{"action": "activation_failed", "error": err.Error()})
|
||||
} else {
|
||||
log.Printf("UI sendMessage succeeded, waiting for server response...")
|
||||
}
|
||||
case "instances":
|
||||
listInstances(dataDir, conn)
|
||||
}
|
||||
@@ -63,30 +80,6 @@ func startUI(dataDir, nodeID, serverAddr string) {
|
||||
}
|
||||
}
|
||||
|
||||
func forwardActivation(serverAddr, nodeID, code string, uiConn *websocket.Conn) {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
|
||||
if err != nil {
|
||||
uiConn.WriteJSON(map[string]interface{}{"action": "activation_failed", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
conn.WriteJSON(map[string]interface{}{"action": "register", "nodeId": nodeID})
|
||||
conn.WriteJSON(map[string]interface{}{"action": "activate", "code": code, "nodeId": nodeID})
|
||||
|
||||
for {
|
||||
var msg map[string]interface{}
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
break
|
||||
}
|
||||
action, _ := msg["action"].(string)
|
||||
if action == "activated" || action == "activation_failed" {
|
||||
uiConn.WriteJSON(msg)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func listInstances(dataDir string, conn *websocket.Conn) {
|
||||
dir := filepath.Join(dataDir, "instances")
|
||||
entries, err := os.ReadDir(dir)
|
||||
|
||||
+91
-10
@@ -1,7 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
@@ -20,6 +22,61 @@ type WSMessage struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
mainConn *websocket.Conn
|
||||
mainConnMu sync.Mutex
|
||||
)
|
||||
|
||||
func sendMessage(msg WSMessage) error {
|
||||
mainConnMu.Lock()
|
||||
defer mainConnMu.Unlock()
|
||||
if mainConn == nil {
|
||||
return fmt.Errorf("not connected to server")
|
||||
}
|
||||
log.Printf("sendMessage: sending %+v", msg)
|
||||
return mainConn.WriteJSON(msg)
|
||||
}
|
||||
|
||||
// UI notifier system: broadcast activation results to all connected UI clients
|
||||
type uiNotifier func(msg map[string]interface{})
|
||||
|
||||
var (
|
||||
uiNotifiers = make(map[int]uiNotifier)
|
||||
uiNotifiersMu sync.Mutex
|
||||
uiNotifierID int
|
||||
)
|
||||
|
||||
func registerUINotifier(fn uiNotifier) int {
|
||||
uiNotifiersMu.Lock()
|
||||
defer uiNotifiersMu.Unlock()
|
||||
id := uiNotifierID
|
||||
uiNotifierID++
|
||||
uiNotifiers[id] = fn
|
||||
log.Printf("registerUINotifier: registered ID %d (total: %d)", id, len(uiNotifiers))
|
||||
return id
|
||||
}
|
||||
|
||||
func unregisterUINotifier(id int) {
|
||||
uiNotifiersMu.Lock()
|
||||
defer uiNotifiersMu.Unlock()
|
||||
delete(uiNotifiers, id)
|
||||
log.Printf("unregisterUINotifier: removed ID %d (total: %d)", id, len(uiNotifiers))
|
||||
}
|
||||
|
||||
func notifyUI(msg map[string]interface{}) {
|
||||
uiNotifiersMu.Lock()
|
||||
notifiers := make([]uiNotifier, 0, len(uiNotifiers))
|
||||
for _, fn := range uiNotifiers {
|
||||
notifiers = append(notifiers, fn)
|
||||
}
|
||||
uiNotifiersMu.Unlock()
|
||||
|
||||
log.Printf("notifyUI: broadcasting to %d UI clients", len(notifiers))
|
||||
for _, fn := range notifiers {
|
||||
go fn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
for {
|
||||
conn, _, err := websocket.DefaultDialer.Dial(serverAddr, nil)
|
||||
@@ -31,10 +88,17 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
|
||||
log.Printf("WS connected to %s", serverAddr)
|
||||
|
||||
mainConnMu.Lock()
|
||||
mainConn = conn
|
||||
mainConnMu.Unlock()
|
||||
|
||||
// Register
|
||||
if err := conn.WriteJSON(WSMessage{Action: "register", NodeID: nodeID}); err != nil {
|
||||
log.Printf("WS register error: %v", err)
|
||||
conn.Close()
|
||||
mainConnMu.Lock()
|
||||
mainConn = nil
|
||||
mainConnMu.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -54,7 +118,7 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
if err := conn.WriteJSON(WSMessage{Action: "heartbeat", NodeID: nodeID}); err != nil {
|
||||
if err := sendMessage(WSMessage{Action: "heartbeat", NodeID: nodeID}); err != nil {
|
||||
return
|
||||
}
|
||||
case <-done:
|
||||
@@ -70,11 +134,15 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
log.Printf("WS read error: %v", err)
|
||||
break
|
||||
}
|
||||
log.Printf("WS received from server: action=%s", msg.Action)
|
||||
handleMessage(conn, msg, dataDir, nodeID)
|
||||
}
|
||||
|
||||
close(done)
|
||||
conn.Close()
|
||||
mainConnMu.Lock()
|
||||
mainConn = nil
|
||||
mainConnMu.Unlock()
|
||||
log.Println("WS disconnected, reconnecting in 5s...")
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
@@ -82,26 +150,39 @@ func startWebSocket(serverAddr, nodeID, dataDir string) {
|
||||
|
||||
func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string) {
|
||||
switch msg.Action {
|
||||
case "activate":
|
||||
// handled by UI, but server can also push activation response
|
||||
case "activated":
|
||||
log.Printf("handleMessage: activated received, student=%s", msg.StudentName)
|
||||
if msg.StudentName != "" {
|
||||
act := &Activation{Activated: true, StudentId: msg.StudentId, StudentName: msg.StudentName, Code: msg.Code}
|
||||
saveActivation(dataDir, act)
|
||||
log.Printf("Activated as %s", msg.StudentName)
|
||||
if err := saveActivation(dataDir, act); err != nil {
|
||||
log.Printf("saveActivation error: %v", err)
|
||||
} else {
|
||||
log.Printf("Activated as %s", msg.StudentName)
|
||||
}
|
||||
}
|
||||
notifyUI(map[string]interface{}{
|
||||
"action": "activated",
|
||||
"studentName": msg.StudentName,
|
||||
})
|
||||
case "activation_failed":
|
||||
log.Printf("handleMessage: activation_failed received, error=%s", msg.Error)
|
||||
notifyUI(map[string]interface{}{
|
||||
"action": "activation_failed",
|
||||
"error": msg.Error,
|
||||
})
|
||||
case "start":
|
||||
log.Printf("Start instance %s on port %d", msg.InstanceID, msg.Port)
|
||||
if err := writeCompose(dataDir, msg.InstanceID, msg.ComposeConfig); err != nil {
|
||||
log.Printf("writeCompose error: %v", err)
|
||||
conn.WriteJSON(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.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)
|
||||
conn.WriteJSON(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
conn.WriteJSON(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
case "stop":
|
||||
log.Printf("Stop instance %s", msg.InstanceID)
|
||||
if err := dockerComposeDown(dataDir, msg.InstanceID); err != nil {
|
||||
@@ -116,10 +197,10 @@ func handleMessage(conn *websocket.Conn, msg WSMessage, dataDir, nodeID string)
|
||||
}
|
||||
if err := dockerComposeUp(dataDir, msg.InstanceID); err != nil {
|
||||
log.Printf("dockerComposeUp error: %v", err)
|
||||
conn.WriteJSON(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
sendMessage(WSMessage{Action: "instance_error", InstanceID: msg.InstanceID, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
conn.WriteJSON(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
sendMessage(WSMessage{Action: "instance_started", InstanceID: msg.InstanceID, Port: msg.Port})
|
||||
default:
|
||||
log.Printf("Unknown action: %s", msg.Action)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user