diff --git a/agent/activation.go b/agent/activation.go index 064a356..fa90dfe 100644 --- a/agent/activation.go +++ b/agent/activation.go @@ -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 { diff --git a/agent/ui.go b/agent/ui.go index fb20385..d27f8cc 100644 --- a/agent/ui.go +++ b/agent/ui.go @@ -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) diff --git a/agent/websocket.go b/agent/websocket.go index ede8640..c38bdef 100644 --- a/agent/websocket.go +++ b/agent/websocket.go @@ -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) } diff --git a/server/app/dashboard/students/[id]/actions.ts b/server/app/dashboard/students/[id]/actions.ts index 7079049..8cf9dec 100644 --- a/server/app/dashboard/students/[id]/actions.ts +++ b/server/app/dashboard/students/[id]/actions.ts @@ -23,12 +23,15 @@ export async function deleteStudent(formData: FormData) { if (!id) return; const establishmentId = session.user.establishmentId; - await prisma.student.deleteMany({ + const student = await prisma.student.findFirst({ where: { id, class: establishmentId ? { establishmentId } : undefined, }, }); + if (!student) return; + + await prisma.student.delete({ where: { id } }); redirect("/dashboard/students"); } diff --git a/server/app/dashboard/students/page.tsx b/server/app/dashboard/students/page.tsx index 8b0f19c..d9b17f2 100644 --- a/server/app/dashboard/students/page.tsx +++ b/server/app/dashboard/students/page.tsx @@ -47,7 +47,11 @@ export default async function StudentsPage() { const node = s.nodes[0]; return ( - {s.firstName} {s.lastName} + + + {s.firstName} {s.lastName} + + {s.class.name} {s.email} {s.activationCode || "-"} diff --git a/server/lib/websocket.ts b/server/lib/websocket.ts index 639d21c..6e8003b 100644 --- a/server/lib/websocket.ts +++ b/server/lib/websocket.ts @@ -18,10 +18,12 @@ const nodes = new Map(); export function initWebSocketServer(wss: WebSocketServer) { wss.on("connection", (ws: WebSocket) => { let nodeId: string | null = null; + console.log("[WS] New connection"); ws.on("message", async (raw) => { try { const msg: NodeMessage = JSON.parse(raw.toString()); + console.log("[WS] Received:", msg.action, "from", msg.nodeId || nodeId); if (msg.action === "register" && msg.nodeId) { nodeId = msg.nodeId; @@ -41,6 +43,7 @@ export function initWebSocketServer(wss: WebSocketServer) { where: { activationCode: msg.code }, }); if (!student) { + console.log("[WS] Invalid code:", msg.code); ws.send(JSON.stringify({ action: "activation_failed", error: "Invalid code" })); return; } @@ -49,6 +52,7 @@ export function initWebSocketServer(wss: WebSocketServer) { update: { studentId: student.id, status: "online", lastSeen: new Date() }, create: { id: nodeId, studentId: student.id, status: "online", lastSeen: new Date() }, }); + console.log("[WS] Activated:", student.firstName, student.lastName, "on", nodeId); ws.send(JSON.stringify({ action: "activated", studentId: student.id, studentName: `${student.firstName} ${student.lastName}` })); return; } @@ -83,6 +87,7 @@ export function initWebSocketServer(wss: WebSocketServer) { }); ws.on("close", async () => { + console.log("[WS] Connection closed for", nodeId); if (nodeId) { nodes.delete(nodeId); await prisma.node.upsert({ diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index e0789af..af7f575 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -63,7 +63,7 @@ model Student { model Node { id String @id studentId String? - student Student? @relation(fields: [studentId], references: [id], onDelete: SetNull) + student Student? @relation(fields: [studentId], references: [id], onDelete: Cascade) tailscaleIp String? status String @default("offline") lastSeen DateTime? diff --git a/server/public/edubox-agent-linux-amd64 b/server/public/edubox-agent-linux-amd64 new file mode 100755 index 0000000..4995367 Binary files /dev/null and b/server/public/edubox-agent-linux-amd64 differ diff --git a/server/public/edubox-agent-mac-amd64 b/server/public/edubox-agent-mac-amd64 new file mode 100755 index 0000000..74289ce Binary files /dev/null and b/server/public/edubox-agent-mac-amd64 differ diff --git a/server/public/edubox-agent-mac-arm64 b/server/public/edubox-agent-mac-arm64 new file mode 100755 index 0000000..a3d6737 Binary files /dev/null and b/server/public/edubox-agent-mac-arm64 differ