···77 "net/http"
88 "os"
99 "path/filepath"
1010+ "runtime/debug"
1011 "sort"
1112 "strings"
1213 "time"
···1516 "github.com/atscan/plcbundle/plc"
1617)
17181818-// Version information (injected at build time via ldflags)
1919+// Version information (injected at build time via ldflags or read from build info)
1920var (
2021 version = "dev"
2122 gitCommit = "unknown"
2223 buildDate = "unknown"
2324)
2525+2626+func init() {
2727+ // Try to get version from build info (works with go install)
2828+ if info, ok := debug.ReadBuildInfo(); ok {
2929+ if info.Main.Version != "" && info.Main.Version != "(devel)" {
3030+ version = info.Main.Version
3131+ }
3232+3333+ // Extract git commit and build time from build settings
3434+ for _, setting := range info.Settings {
3535+ switch setting.Key {
3636+ case "vcs.revision":
3737+ if setting.Value != "" {
3838+ gitCommit = setting.Value
3939+ if len(gitCommit) > 7 {
4040+ gitCommit = gitCommit[:7] // Short hash
4141+ }
4242+ }
4343+ case "vcs.time":
4444+ if setting.Value != "" {
4545+ buildDate = setting.Value
4646+ }
4747+ }
4848+ }
4949+ }
5050+}
24512552func main() {
2653 if len(os.Args) < 2 {
+61-25
cmd/plcbundle/server.go
···1212 "time"
13131414 "github.com/atscan/plcbundle/bundle"
1515+ "github.com/atscan/plcbundle/plc"
1516 "github.com/gorilla/websocket"
1617)
1718···143144 index := mgr.GetIndex()
144145 bundles := index.GetBundles()
145146146146- if len(bundles) == 0 {
147147- return
148148- }
149149-150147 currentRecord := 0
151148152149 // Stream all operations from all bundles
···166163 continue
167164 }
168165169169- // Send raw JSON if available, otherwise marshal the operation
170170- var data []byte
171171- if len(op.RawJSON) > 0 {
172172- data = op.RawJSON
173173- } else {
174174- data, err = json.Marshal(op)
175175- if err != nil {
176176- fmt.Fprintf(os.Stderr, "Failed to marshal operation: %v\n", err)
177177- currentRecord++
178178- continue
179179- }
180180- }
181181-182182- // Send raw JSON as text message
183183- if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
184184- // Connection closed or error
185185- fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err)
166166+ // Send raw JSON
167167+ if err := sendOperation(conn, op); err != nil {
186168 return
187169 }
188170189171 currentRecord++
190172191191- // Optional: Send ping periodically to keep connection alive
173173+ // Send ping periodically to keep connection alive
192174 if currentRecord%1000 == 0 {
193175 if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
194176 return
···196178 }
197179 }
198180 }
181181+182182+ // Stream mempool operations (seamlessly after bundles)
183183+ mempoolOps, err := mgr.GetMempoolOperations()
184184+ if err != nil {
185185+ fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err)
186186+ return
187187+ }
188188+189189+ for _, op := range mempoolOps {
190190+ // Skip records before cursor
191191+ if currentRecord < cursor {
192192+ currentRecord++
193193+ continue
194194+ }
195195+196196+ // Send raw JSON
197197+ if err := sendOperation(conn, op); err != nil {
198198+ return
199199+ }
200200+201201+ currentRecord++
202202+203203+ // Send ping periodically
204204+ if currentRecord%1000 == 0 {
205205+ if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
206206+ return
207207+ }
208208+ }
209209+ }
210210+}
211211+212212+// sendOperation sends a single operation over WebSocket as raw JSON
213213+func sendOperation(conn *websocket.Conn, op plc.PLCOperation) error {
214214+ var data []byte
215215+ var err error
216216+217217+ // Use raw JSON if available, otherwise marshal
218218+ if len(op.RawJSON) > 0 {
219219+ data = op.RawJSON
220220+ } else {
221221+ data, err = json.Marshal(op)
222222+ if err != nil {
223223+ fmt.Fprintf(os.Stderr, "Failed to marshal operation: %v\n", err)
224224+ return nil // Skip this operation but continue
225225+ }
226226+ }
227227+228228+ // Send as text message
229229+ if err := conn.WriteMessage(websocket.TextMessage, data); err != nil {
230230+ fmt.Fprintf(os.Stderr, "WebSocket write error: %v\n", err)
231231+ return err
232232+ }
233233+234234+ return nil
199235}
200236201237func handleRoot(w http.ResponseWriter, r *http.Request, mgr *bundle.Manager, syncMode bool, wsEnabled bool) {
···315351 fmt.Fprintf(w, "━━━━━━━━\n")
316352 fmt.Fprintf(w, " # Get bundle metadata\n")
317353 fmt.Fprintf(w, " curl %s/bundle/1\n\n", baseURL)
318318- fmt.Fprintf(w, " # Download compressed bundle\n")
319319- fmt.Fprintf(w, " curl %s/data/1 -o 000001.jsonl.zst\n\n", baseURL)
320320- fmt.Fprintf(w, " # Stream decompressed operations\n")
354354+ fmt.Fprintf(w, " # Download compressed bundle 42\n")
355355+ fmt.Fprintf(w, " curl %s/data/42 -o 000042.jsonl.zst\n\n", baseURL)
356356+ fmt.Fprintf(w, " # Stream decompressed operations from bundle 42\n")
321357 fmt.Fprintf(w, " curl %s/jsonl/1\n\n", baseURL)
322358323359 if wsEnabled {