# SSE Events

This page describes the Server-Sent Events format go-pve-qemu uses for long-running operations and how to consume it from Go.

Endpoints using SSE: `install`, `start`, `reboot`, `set/node`.

## Event Format

```text
data: {"step":"<stage>","status":"<status>","message":"<message>"}

event: close
data: {}
```

| Field | Description |
|---|---|
| `step` | Step name; the install flow uses `<stage> > <step>`, see [Provisioning Pipeline](/provisioning-pipeline) |
| `status` | See the table below |
| `message` | Human-readable message; finished steps include the duration, e.g. `(3.21s)` |

| `status` | Meaning |
|---|---|
| `processing` | Step in progress, or a line of `qm` / init script output (indented by two spaces) |
| `success` | Step finished |
| `info` | Final notice (start / reboot) |
| `error` | Step failed; the flow stops |

| `message` prefix | Meaning |
|---|---|
| `[*]` | Info |
| `[+]` | Success |
| `[-]` | Failure |

## Notes

- Responses are always HTTP 200; read the events to tell success from failure
- `message` is built by string concatenation without JSON escaping; when command output contains `"` or `\` that line may not parse as JSON, so clients should skip lines that fail to parse
- When a pre-check fails (`ALLOW_IPS`, VM state) you get a single `data: {"message": "..."}` line with no `step` / `status`
- If the client disconnects, the server-side flow keeps running and simply stops pushing events

## Go Client Example

```go
package main

import (
	"bufio"
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"strings"
)

type event struct {
	Step    string `json:"step"`
	Status  string `json:"status"`
	Message string `json:"message"`
}

func main() {
	body, err := json.Marshal(map[string]any{
		"os":      "ubuntu",
		"version": "24.04",
		"cpu":     2,
		"ram":     4096,
		"disk":    "32G",
	})
	if err != nil {
		log.Fatal(err)
	}

	resp, err := http.Post("http://192.168.0.11:8080/api/vm/install", "application/json", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		log.Fatalf("install rejected: %s", resp.Status)
	}

	failed := false
	scanner := bufio.NewScanner(resp.Body)
	for scanner.Scan() {
		line := scanner.Text()
		if line == "event: close" {
			// server-side flow finished
			break
		}
		payload, ok := strings.CutPrefix(line, "data: ")
		if !ok {
			continue
		}
		var e event
		if err := json.Unmarshal([]byte(payload), &e); err != nil {
			// unescaped command output, skip
			continue
		}
		fmt.Printf("[%s] %s %s\n", e.Status, e.Step, e.Message)
		if e.Status == "error" {
			failed = true
		}
	}
	if err := scanner.Err(); err != nil {
		log.Fatal(err)
	}
	if failed {
		log.Fatal("install failed")
	}
}
```
