tangled
alpha
login
or
join now
atscan.net
/
plcbundle
A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory
14
fork
atom
overview
issues
2
pulls
pipelines
better info, ws update
tree.fail
3 months ago
db5f7e16
cb6e2c64
+512
-95
10 changed files
expand all
collapse all
unified
split
bundle
bundle_test.go
manager.go
operations.go
types.go
cmd
plcbundle
info.go
main.go
server.go
plc
plc_test.go
plc_bundles.json
plcbundle.go
+1
-1
bundle/bundle_test.go
···
311
tmpDir := t.TempDir()
312
logger := &testLogger{t: t}
313
314
-
ops, err := bundle.NewOperations(bundle.CompressionBetter, logger)
315
if err != nil {
316
t.Fatalf("NewOperations failed: %v", err)
317
}
···
311
tmpDir := t.TempDir()
312
logger := &testLogger{t: t}
313
314
+
ops, err := bundle.NewOperations(logger)
315
if err != nil {
316
t.Fatalf("NewOperations failed: %v", err)
317
}
+1
-2
bundle/manager.go
···
53
}
54
55
// Initialize operations handler
56
-
ops, err := NewOperations(config.CompressionLevel, config.Logger)
57
if err != nil {
58
return nil, fmt.Errorf("failed to initialize operations: %w", err)
59
}
···
640
stats := m.index.GetStats()
641
stats["bundle_dir"] = m.config.BundleDir
642
stats["index_path"] = m.indexPath
643
-
stats["compression_level"] = m.config.CompressionLevel
644
stats["verify_on_load"] = m.config.VerifyOnLoad
645
return stats
646
}
···
53
}
54
55
// Initialize operations handler
56
+
ops, err := NewOperations(config.Logger)
57
if err != nil {
58
return nil, fmt.Errorf("failed to initialize operations: %w", err)
59
}
···
640
stats := m.index.GetStats()
641
stats["bundle_dir"] = m.config.BundleDir
642
stats["index_path"] = m.indexPath
0
643
stats["verify_on_load"] = m.config.VerifyOnLoad
644
return stats
645
}
+4
-17
bundle/operations.go
···
22
logger Logger
23
}
24
25
-
// NewOperations creates a new Operations handler
26
-
func NewOperations(compressionLevel CompressionLevel, logger Logger) (*Operations, error) {
27
-
var zstdLevel zstd.EncoderLevel
28
-
switch compressionLevel {
29
-
case CompressionFastest:
30
-
zstdLevel = zstd.SpeedFastest
31
-
case CompressionDefault:
32
-
zstdLevel = zstd.SpeedDefault
33
-
case CompressionBetter:
34
-
zstdLevel = zstd.SpeedBetterCompression
35
-
case CompressionBest:
36
-
zstdLevel = zstd.SpeedBestCompression
37
-
default:
38
-
zstdLevel = zstd.SpeedBetterCompression
39
-
}
40
-
41
-
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstdLevel))
42
if err != nil {
43
return nil, fmt.Errorf("failed to create zstd encoder: %w", err)
44
}
···
22
logger Logger
23
}
24
25
+
// NewOperations creates a new Operations handler with default compression
26
+
func NewOperations(logger Logger) (*Operations, error) {
27
+
// Always use default compression (level 3 - good balance)
28
+
encoder, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedDefault))
0
0
0
0
0
0
0
0
0
0
0
0
0
29
if err != nil {
30
return nil, fmt.Errorf("failed to create zstd encoder: %w", err)
31
}
+6
-21
bundle/types.go
···
11
const (
12
// BUNDLE_SIZE is the standard number of operations per bundle
13
BUNDLE_SIZE = 10000
14
-
15
-
// DefaultCompression is the default compression level
16
-
DefaultCompression = CompressionBetter
17
-
)
18
-
19
-
// CompressionLevel represents zstd compression levels
20
-
type CompressionLevel int
21
-
22
-
const (
23
-
CompressionFastest CompressionLevel = 1
24
-
CompressionDefault CompressionLevel = 3
25
-
CompressionBetter CompressionLevel = 5
26
-
CompressionBest CompressionLevel = 9
27
)
28
29
// Bundle represents a PLC bundle
···
171
172
// Config holds configuration for bundle operations
173
type Config struct {
174
-
BundleDir string
175
-
CompressionLevel CompressionLevel
176
-
VerifyOnLoad bool
177
-
Logger Logger
178
}
179
180
// DefaultConfig returns default configuration
181
func DefaultConfig(bundleDir string) *Config {
182
return &Config{
183
-
BundleDir: bundleDir,
184
-
CompressionLevel: CompressionBetter,
185
-
VerifyOnLoad: true,
186
-
Logger: nil, // Will use defaultLogger in manager
187
}
188
}
···
11
const (
12
// BUNDLE_SIZE is the standard number of operations per bundle
13
BUNDLE_SIZE = 10000
0
0
0
0
0
0
0
0
0
0
0
0
0
14
)
15
16
// Bundle represents a PLC bundle
···
158
159
// Config holds configuration for bundle operations
160
type Config struct {
161
+
BundleDir string
162
+
VerifyOnLoad bool
163
+
Logger Logger
0
164
}
165
166
// DefaultConfig returns default configuration
167
func DefaultConfig(bundleDir string) *Config {
168
return &Config{
169
+
BundleDir: bundleDir,
170
+
VerifyOnLoad: true,
171
+
Logger: nil, // Will use defaultLogger in manager
0
172
}
173
}
+334
cmd/plcbundle/info.go
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
···
1
+
package main
2
+
3
+
import (
4
+
"context"
5
+
"fmt"
6
+
"os"
7
+
"path/filepath"
8
+
"sort"
9
+
"strings"
10
+
"time"
11
+
12
+
"github.com/atscan/plcbundle/bundle"
13
+
)
14
+
15
+
func showGeneralInfo(mgr *bundle.Manager, dir string, verbose bool, showBundles bool, verify bool, showTimeline bool) {
16
+
index := mgr.GetIndex()
17
+
info := mgr.GetInfo()
18
+
stats := index.GetStats()
19
+
bundleCount := stats["bundle_count"].(int)
20
+
21
+
fmt.Printf("\n")
22
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n")
23
+
fmt.Printf(" PLC Bundle Repository Overview\n")
24
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n")
25
+
fmt.Printf("\n")
26
+
27
+
// Location
28
+
fmt.Printf("📁 Location\n")
29
+
fmt.Printf(" Directory: %s\n", dir)
30
+
fmt.Printf(" Index: %s\n", filepath.Base(info["index_path"].(string)))
31
+
fmt.Printf("\n")
32
+
33
+
if bundleCount == 0 {
34
+
fmt.Printf("⚠️ No bundles found\n")
35
+
fmt.Printf("\n")
36
+
fmt.Printf("Get started:\n")
37
+
fmt.Printf(" plcbundle fetch # Fetch bundles from PLC\n")
38
+
fmt.Printf(" plcbundle scan # Scan existing bundle files\n")
39
+
fmt.Printf("\n")
40
+
return
41
+
}
42
+
43
+
firstBundle := stats["first_bundle"].(int)
44
+
lastBundle := stats["last_bundle"].(int)
45
+
totalSize := stats["total_size"].(int64)
46
+
startTime := stats["start_time"].(time.Time)
47
+
endTime := stats["end_time"].(time.Time)
48
+
updatedAt := stats["updated_at"].(time.Time)
49
+
50
+
// Summary
51
+
fmt.Printf("📊 Summary\n")
52
+
fmt.Printf(" Bundles: %s\n", formatNumber(bundleCount))
53
+
fmt.Printf(" Range: %06d → %06d\n", firstBundle, lastBundle)
54
+
fmt.Printf(" Total Size: %s\n", formatBytes(totalSize))
55
+
fmt.Printf(" Avg/Bundle: %s\n", formatBytes(totalSize/int64(bundleCount)))
56
+
fmt.Printf("\n")
57
+
58
+
// Timeline
59
+
duration := endTime.Sub(startTime)
60
+
fmt.Printf("📅 Timeline\n")
61
+
fmt.Printf(" First Op: %s\n", startTime.Format("2006-01-02 15:04:05 MST"))
62
+
fmt.Printf(" Last Op: %s\n", endTime.Format("2006-01-02 15:04:05 MST"))
63
+
fmt.Printf(" Timespan: %s\n", formatDuration(duration))
64
+
fmt.Printf(" Last Updated: %s\n", updatedAt.Format("2006-01-02 15:04:05 MST"))
65
+
fmt.Printf(" Age: %s ago\n", formatDuration(time.Since(updatedAt)))
66
+
fmt.Printf("\n")
67
+
68
+
// Operations estimate
69
+
totalOps := bundleCount * bundle.BUNDLE_SIZE
70
+
fmt.Printf("🔢 Operations (estimate)\n")
71
+
fmt.Printf(" Total: %s\n", formatNumber(totalOps))
72
+
if duration.Hours() > 0 {
73
+
opsPerHour := float64(totalOps) / duration.Hours()
74
+
fmt.Printf(" Rate: %.0f ops/hour\n", opsPerHour)
75
+
}
76
+
fmt.Printf("\n")
77
+
78
+
// Gaps
79
+
gaps := index.FindGaps()
80
+
if len(gaps) > 0 {
81
+
fmt.Printf("⚠️ Missing Bundles: %d\n", len(gaps))
82
+
if len(gaps) <= 10 || verbose {
83
+
fmt.Printf(" ")
84
+
for i, gap := range gaps {
85
+
if i > 0 {
86
+
fmt.Printf(", ")
87
+
}
88
+
if i > 0 && i%10 == 0 {
89
+
fmt.Printf("\n ")
90
+
}
91
+
fmt.Printf("%06d", gap)
92
+
}
93
+
fmt.Printf("\n")
94
+
} else {
95
+
fmt.Printf(" First few: ")
96
+
for i := 0; i < 5; i++ {
97
+
if i > 0 {
98
+
fmt.Printf(", ")
99
+
}
100
+
fmt.Printf("%06d", gaps[i])
101
+
}
102
+
fmt.Printf(" ... (use -v to show all)\n")
103
+
}
104
+
fmt.Printf("\n")
105
+
}
106
+
107
+
// Mempool
108
+
mempoolStats := mgr.GetMempoolStats()
109
+
mempoolCount := mempoolStats["count"].(int)
110
+
if mempoolCount > 0 {
111
+
targetBundle := mempoolStats["target_bundle"].(int)
112
+
canCreate := mempoolStats["can_create_bundle"].(bool)
113
+
progress := float64(mempoolCount) / float64(bundle.BUNDLE_SIZE) * 100
114
+
115
+
fmt.Printf("🔄 Mempool (next bundle: %06d)\n", targetBundle)
116
+
fmt.Printf(" Operations: %s / %s\n", formatNumber(mempoolCount), formatNumber(bundle.BUNDLE_SIZE))
117
+
fmt.Printf(" Progress: %.1f%%\n", progress)
118
+
119
+
// Progress bar
120
+
barWidth := 40
121
+
filled := int(float64(barWidth) * float64(mempoolCount) / float64(bundle.BUNDLE_SIZE))
122
+
if filled > barWidth {
123
+
filled = barWidth
124
+
}
125
+
bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled)
126
+
fmt.Printf(" [%s]\n", bar)
127
+
128
+
if canCreate {
129
+
fmt.Printf(" ✓ Ready to create bundle\n")
130
+
} else {
131
+
remaining := bundle.BUNDLE_SIZE - mempoolCount
132
+
fmt.Printf(" Need %s more operations\n", formatNumber(remaining))
133
+
}
134
+
fmt.Printf("\n")
135
+
}
136
+
137
+
// Chain verification
138
+
if verify {
139
+
fmt.Printf("🔐 Chain Verification\n")
140
+
fmt.Printf(" Verifying %d bundles...\n", bundleCount)
141
+
142
+
ctx := context.Background()
143
+
result, err := mgr.VerifyChain(ctx)
144
+
if err != nil {
145
+
fmt.Printf(" ✗ Verification failed: %v\n", err)
146
+
} else if result.Valid {
147
+
fmt.Printf(" ✓ Chain is valid\n")
148
+
fmt.Printf(" ✓ All %d bundles verified\n", len(result.VerifiedBundles))
149
+
150
+
// Show head hash
151
+
lastMeta, _ := index.GetBundle(lastBundle)
152
+
if lastMeta != nil {
153
+
fmt.Printf(" Head: %s\n", lastMeta.Hash[:16]+"...")
154
+
}
155
+
} else {
156
+
fmt.Printf(" ✗ Chain is broken\n")
157
+
fmt.Printf(" Verified: %d/%d bundles\n", len(result.VerifiedBundles), bundleCount)
158
+
fmt.Printf(" Broken at: %06d\n", result.BrokenAt)
159
+
fmt.Printf(" Error: %s\n", result.Error)
160
+
}
161
+
fmt.Printf("\n")
162
+
}
163
+
164
+
// Timeline visualization
165
+
if showTimeline {
166
+
fmt.Printf("📈 Timeline Visualization\n")
167
+
visualizeTimeline(index, verbose)
168
+
fmt.Printf("\n")
169
+
}
170
+
171
+
// Bundle list
172
+
if showBundles {
173
+
bundles := index.GetBundles()
174
+
fmt.Printf("📚 Bundle List (%d total)\n", len(bundles))
175
+
fmt.Printf("\n")
176
+
fmt.Printf(" Number | Start Time | End Time | Ops | DIDs | Size\n")
177
+
fmt.Printf(" ---------|---------------------|---------------------|--------|--------|--------\n")
178
+
179
+
for _, meta := range bundles {
180
+
fmt.Printf(" %06d | %s | %s | %6d | %6d | %7s\n",
181
+
meta.BundleNumber,
182
+
meta.StartTime.Format("2006-01-02 15:04"),
183
+
meta.EndTime.Format("2006-01-02 15:04"),
184
+
meta.OperationCount,
185
+
meta.DIDCount,
186
+
formatBytes(meta.CompressedSize))
187
+
}
188
+
fmt.Printf("\n")
189
+
} else if bundleCount > 0 {
190
+
fmt.Printf("💡 Tip: Use --bundles to see detailed bundle list\n")
191
+
fmt.Printf(" Use --timeline to see timeline visualization\n")
192
+
fmt.Printf(" Use --verify to verify chain integrity\n")
193
+
fmt.Printf("\n")
194
+
}
195
+
196
+
// File system stats (verbose)
197
+
if verbose {
198
+
fmt.Printf("💾 File System\n")
199
+
200
+
// Calculate average compression ratio
201
+
bundles := index.GetBundles()
202
+
var totalCompressed, totalUncompressed int64
203
+
for _, meta := range bundles {
204
+
totalCompressed += meta.CompressedSize
205
+
totalUncompressed += meta.UncompressedSize
206
+
}
207
+
if totalCompressed > 0 {
208
+
avgRatio := float64(totalUncompressed) / float64(totalCompressed)
209
+
savings := (1 - float64(totalCompressed)/float64(totalUncompressed)) * 100
210
+
fmt.Printf(" Compression: %.2fx average ratio\n", avgRatio)
211
+
fmt.Printf(" Space Saved: %.1f%% (%s)\n", savings, formatBytes(totalUncompressed-totalCompressed))
212
+
}
213
+
214
+
// Index size
215
+
indexPath := info["index_path"].(string)
216
+
if indexInfo, err := os.Stat(indexPath); err == nil {
217
+
fmt.Printf(" Index Size: %s\n", formatBytes(indexInfo.Size()))
218
+
}
219
+
220
+
fmt.Printf("\n")
221
+
}
222
+
}
223
+
224
+
func visualizeTimeline(index *bundle.Index, verbose bool) {
225
+
bundles := index.GetBundles()
226
+
if len(bundles) == 0 {
227
+
return
228
+
}
229
+
230
+
// Group bundles by date
231
+
type dateGroup struct {
232
+
date string
233
+
count int
234
+
first int
235
+
last int
236
+
}
237
+
238
+
dateMap := make(map[string]*dateGroup)
239
+
for _, meta := range bundles {
240
+
dateStr := meta.StartTime.Format("2006-01-02")
241
+
if group, exists := dateMap[dateStr]; exists {
242
+
group.count++
243
+
group.last = meta.BundleNumber
244
+
} else {
245
+
dateMap[dateStr] = &dateGroup{
246
+
date: dateStr,
247
+
count: 1,
248
+
first: meta.BundleNumber,
249
+
last: meta.BundleNumber,
250
+
}
251
+
}
252
+
}
253
+
254
+
// Sort dates
255
+
var dates []string
256
+
for date := range dateMap {
257
+
dates = append(dates, date)
258
+
}
259
+
sort.Strings(dates)
260
+
261
+
// Find max count for scaling
262
+
maxCount := 0
263
+
for _, group := range dateMap {
264
+
if group.count > maxCount {
265
+
maxCount = group.count
266
+
}
267
+
}
268
+
269
+
// Display
270
+
fmt.Printf("\n")
271
+
barWidth := 40
272
+
for _, date := range dates {
273
+
group := dateMap[date]
274
+
barLen := int(float64(barWidth) * float64(group.count) / float64(maxCount))
275
+
if barLen == 0 && group.count > 0 {
276
+
barLen = 1
277
+
}
278
+
279
+
bar := strings.Repeat("█", barLen)
280
+
fmt.Printf(" %s | %-40s | %3d bundles", group.date, bar, group.count)
281
+
if verbose {
282
+
fmt.Printf(" (%06d-%06d)", group.first, group.last)
283
+
}
284
+
fmt.Printf("\n")
285
+
}
286
+
}
287
+
288
+
// Helper formatting functions
289
+
290
+
func formatNumber(n int) string {
291
+
s := fmt.Sprintf("%d", n)
292
+
// Add thousand separators
293
+
var result []byte
294
+
for i, c := range s {
295
+
if i > 0 && (len(s)-i)%3 == 0 {
296
+
result = append(result, ',')
297
+
}
298
+
result = append(result, byte(c))
299
+
}
300
+
return string(result)
301
+
}
302
+
303
+
func formatBytes(bytes int64) string {
304
+
const unit = 1024
305
+
if bytes < unit {
306
+
return fmt.Sprintf("%d B", bytes)
307
+
}
308
+
div, exp := int64(unit), 0
309
+
for n := bytes / unit; n >= unit; n /= unit {
310
+
div *= unit
311
+
exp++
312
+
}
313
+
return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp])
314
+
}
315
+
316
+
func formatDuration(d time.Duration) string {
317
+
if d < time.Minute {
318
+
return fmt.Sprintf("%.0f seconds", d.Seconds())
319
+
}
320
+
if d < time.Hour {
321
+
return fmt.Sprintf("%.1f minutes", d.Minutes())
322
+
}
323
+
if d < 24*time.Hour {
324
+
return fmt.Sprintf("%.1f hours", d.Hours())
325
+
}
326
+
days := d.Hours() / 24
327
+
if days < 30 {
328
+
return fmt.Sprintf("%.1f days", days)
329
+
}
330
+
if days < 365 {
331
+
return fmt.Sprintf("%.1f months", days/30)
332
+
}
333
+
return fmt.Sprintf("%.1f years", days/365)
334
+
}
+127
-29
cmd/plcbundle/main.go
···
474
func cmdInfo() {
475
fs := flag.NewFlagSet("info", flag.ExitOnError)
476
bundleNum := fs.Int("bundle", 0, "specific bundle info (0 = general info)")
0
0
0
0
477
fs.Parse(os.Args[2:])
478
479
mgr, dir, err := getManager("")
···
484
defer mgr.Close()
485
486
if *bundleNum > 0 {
487
-
// Show specific bundle info
488
-
ctx := context.Background()
489
-
b, err := mgr.LoadBundle(ctx, *bundleNum)
490
-
if err != nil {
491
-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
492
-
os.Exit(1)
493
-
}
0
0
0
0
0
0
494
495
-
fmt.Printf("Bundle %06d:\n", b.BundleNumber)
496
-
fmt.Printf(" Directory: %s\n", dir)
497
-
fmt.Printf(" Time range: %s - %s\n", b.StartTime.Format("2006-01-02 15:04:05"), b.EndTime.Format("2006-01-02 15:04:05"))
498
-
fmt.Printf(" Operations: %d\n", len(b.Operations))
499
-
fmt.Printf(" Unique DIDs: %d\n", b.DIDCount)
500
-
fmt.Printf(" Hash: %s\n", b.Hash)
501
-
fmt.Printf(" Compressed: %.2f MB\n", float64(b.CompressedSize)/(1024*1024))
502
-
fmt.Printf(" Uncompressed: %.2f MB\n", float64(b.UncompressedSize)/(1024*1024))
503
-
fmt.Printf(" Compression ratio: %.2fx\n", b.CompressionRatio())
504
-
fmt.Printf(" Cursor: %s\n", b.Cursor)
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
505
if b.PrevBundleHash != "" {
506
-
fmt.Printf(" Prev bundle hash: %s...\n", b.PrevBundleHash[:16])
0
0
0
0
0
0
0
0
0
0
0
0
0
507
}
508
-
} else {
509
-
// Show general info
510
-
info := mgr.GetInfo()
511
-
fmt.Printf("Bundle Directory: %s\n", dir)
512
-
fmt.Printf("Bundle count: %v\n", info["bundle_count"])
513
-
if bc, ok := info["bundle_count"].(int); ok && bc > 0 {
514
-
fmt.Printf("Range: %06d - %06d\n", info["first_bundle"], info["last_bundle"])
515
-
fmt.Printf("Total size: %.2f MB\n", float64(info["total_size"].(int64))/(1024*1024))
516
-
if gaps, ok := info["gaps"].(int); ok && gaps > 0 {
517
-
fmt.Printf("⚠ Missing bundles: %d\n", gaps)
518
}
519
}
520
-
fmt.Printf("Index updated: %s\n", info["updated_at"])
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
521
}
522
}
523
···
474
func cmdInfo() {
475
fs := flag.NewFlagSet("info", flag.ExitOnError)
476
bundleNum := fs.Int("bundle", 0, "specific bundle info (0 = general info)")
477
+
verbose := fs.Bool("v", false, "verbose output")
478
+
showBundles := fs.Bool("bundles", false, "show bundle list")
479
+
verify := fs.Bool("verify", false, "verify chain integrity")
480
+
showTimeline := fs.Bool("timeline", false, "show timeline visualization")
481
fs.Parse(os.Args[2:])
482
483
mgr, dir, err := getManager("")
···
488
defer mgr.Close()
489
490
if *bundleNum > 0 {
491
+
showBundleInfo(mgr, dir, *bundleNum, *verbose)
492
+
} else {
493
+
showGeneralInfo(mgr, dir, *verbose, *showBundles, *verify, *showTimeline)
494
+
}
495
+
}
496
+
497
+
func showBundleInfo(mgr *bundle.Manager, dir string, bundleNum int, verbose bool) {
498
+
ctx := context.Background()
499
+
b, err := mgr.LoadBundle(ctx, bundleNum)
500
+
if err != nil {
501
+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
502
+
os.Exit(1)
503
+
}
504
505
+
fmt.Printf("\n")
506
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n")
507
+
fmt.Printf(" Bundle %06d\n", b.BundleNumber)
508
+
fmt.Printf("═══════════════════════════════════════════════════════════════\n")
509
+
fmt.Printf("\n")
510
+
511
+
// Location
512
+
fmt.Printf("📁 Location\n")
513
+
fmt.Printf(" Directory: %s\n", dir)
514
+
fmt.Printf(" File: %06d.jsonl.zst\n", b.BundleNumber)
515
+
fmt.Printf("\n")
516
+
517
+
// Time Range
518
+
duration := b.EndTime.Sub(b.StartTime)
519
+
fmt.Printf("📅 Time Range\n")
520
+
fmt.Printf(" Start: %s\n", b.StartTime.Format("2006-01-02 15:04:05.000 MST"))
521
+
fmt.Printf(" End: %s\n", b.EndTime.Format("2006-01-02 15:04:05.000 MST"))
522
+
fmt.Printf(" Duration: %s\n", formatDuration(duration))
523
+
fmt.Printf(" Created: %s\n", b.CreatedAt.Format("2006-01-02 15:04:05 MST"))
524
+
fmt.Printf("\n")
525
+
526
+
// Content
527
+
fmt.Printf("📊 Content\n")
528
+
fmt.Printf(" Operations: %s\n", formatNumber(len(b.Operations)))
529
+
fmt.Printf(" Unique DIDs: %s\n", formatNumber(b.DIDCount))
530
+
if len(b.Operations) > 0 {
531
+
avgOpsPerDID := float64(len(b.Operations)) / float64(b.DIDCount)
532
+
fmt.Printf(" Avg ops/DID: %.2f\n", avgOpsPerDID)
533
+
}
534
+
fmt.Printf("\n")
535
+
536
+
// Size
537
+
fmt.Printf("💾 Size\n")
538
+
fmt.Printf(" Compressed: %s\n", formatBytes(b.CompressedSize))
539
+
fmt.Printf(" Uncompressed: %s\n", formatBytes(b.UncompressedSize))
540
+
fmt.Printf(" Ratio: %.2fx\n", b.CompressionRatio())
541
+
fmt.Printf(" Efficiency: %.1f%% savings\n", (1-float64(b.CompressedSize)/float64(b.UncompressedSize))*100)
542
+
fmt.Printf("\n")
543
+
544
+
// Hashes
545
+
fmt.Printf("🔐 Cryptographic Hashes\n")
546
+
fmt.Printf(" Content (SHA-256):\n")
547
+
fmt.Printf(" %s\n", b.Hash)
548
+
fmt.Printf(" Compressed:\n")
549
+
fmt.Printf(" %s\n", b.CompressedHash)
550
+
if b.PrevBundleHash != "" {
551
+
fmt.Printf(" Previous Bundle:\n")
552
+
fmt.Printf(" %s\n", b.PrevBundleHash)
553
+
}
554
+
fmt.Printf("\n")
555
+
556
+
// Chain
557
+
if b.PrevBundleHash != "" || b.Cursor != "" {
558
+
fmt.Printf("🔗 Chain Information\n")
559
+
if b.Cursor != "" {
560
+
fmt.Printf(" Cursor: %s\n", b.Cursor)
561
+
}
562
if b.PrevBundleHash != "" {
563
+
fmt.Printf(" Links to: Bundle %06d\n", bundleNum-1)
564
+
}
565
+
if len(b.BoundaryCIDs) > 0 {
566
+
fmt.Printf(" Boundary: %d CIDs at same timestamp\n", len(b.BoundaryCIDs))
567
+
}
568
+
fmt.Printf("\n")
569
+
}
570
+
571
+
// Verbose: Show sample operations
572
+
if verbose && len(b.Operations) > 0 {
573
+
fmt.Printf("📝 Sample Operations (first 5)\n")
574
+
showCount := 5
575
+
if len(b.Operations) < showCount {
576
+
showCount = len(b.Operations)
577
}
578
+
for i := 0; i < showCount; i++ {
579
+
op := b.Operations[i]
580
+
fmt.Printf(" %d. %s\n", i+1, op.DID)
581
+
fmt.Printf(" CID: %s\n", op.CID)
582
+
fmt.Printf(" Time: %s\n", op.CreatedAt.Format("2006-01-02 15:04:05.000"))
583
+
if op.IsNullified() {
584
+
fmt.Printf(" ⚠️ Nullified: %s\n", op.GetNullifyingCID())
0
0
0
585
}
586
}
587
+
fmt.Printf("\n")
588
+
}
589
+
590
+
// Verbose: Show DID statistics
591
+
if verbose && len(b.Operations) > 0 {
592
+
didOps := make(map[string]int)
593
+
for _, op := range b.Operations {
594
+
didOps[op.DID]++
595
+
}
596
+
597
+
// Find most active DIDs
598
+
type didCount struct {
599
+
did string
600
+
count int
601
+
}
602
+
var counts []didCount
603
+
for did, count := range didOps {
604
+
counts = append(counts, didCount{did, count})
605
+
}
606
+
sort.Slice(counts, func(i, j int) bool {
607
+
return counts[i].count > counts[j].count
608
+
})
609
+
610
+
fmt.Printf("🏆 Most Active DIDs\n")
611
+
showCount := 5
612
+
if len(counts) < showCount {
613
+
showCount = len(counts)
614
+
}
615
+
for i := 0; i < showCount; i++ {
616
+
fmt.Printf(" %d. %s (%d ops)\n", i+1, counts[i].did, counts[i].count)
617
+
}
618
+
fmt.Printf("\n")
619
}
620
}
621
+38
-11
cmd/plcbundle/server.go
···
144
index := mgr.GetIndex()
145
bundles := index.GetBundles()
146
147
-
currentRecord := 0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
148
149
-
// Stream all operations from all bundles
150
-
for _, meta := range bundles {
0
0
0
0
151
// Load bundle
152
b, err := mgr.LoadBundle(ctx, meta.BundleNumber)
153
if err != nil {
···
155
continue
156
}
157
158
-
// Send each operation
159
-
for _, op := range b.Operations {
160
-
// Skip records before cursor
161
-
if currentRecord < cursor {
162
-
currentRecord++
163
-
continue
164
-
}
0
0
165
166
// Send raw JSON
167
if err := sendOperation(conn, op); err != nil {
···
179
}
180
}
181
182
-
// Stream mempool operations (seamlessly after bundles)
0
0
0
0
0
183
mempoolOps, err := mgr.GetMempoolOperations()
184
if err != nil {
185
fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err)
···
144
index := mgr.GetIndex()
145
bundles := index.GetBundles()
146
147
+
if len(bundles) == 0 {
148
+
return
149
+
}
150
+
151
+
// Calculate starting bundle and position from cursor
152
+
// Each bundle has exactly BUNDLE_SIZE (10,000) operations
153
+
// Cursor 88410345 = bundle 8841, position 345
154
+
startBundleIdx := cursor / bundle.BUNDLE_SIZE
155
+
startPosition := cursor % bundle.BUNDLE_SIZE
156
+
157
+
// Validate starting bundle exists
158
+
if startBundleIdx >= len(bundles) {
159
+
// Cursor is beyond all bundles, check mempool
160
+
currentRecord := len(bundles) * bundle.BUNDLE_SIZE
161
+
streamMempool(conn, mgr, cursor, currentRecord)
162
+
return
163
+
}
164
165
+
currentRecord := cursor
166
+
167
+
// Stream bundles starting from the calculated bundle
168
+
for i := startBundleIdx; i < len(bundles); i++ {
169
+
meta := bundles[i]
170
+
171
// Load bundle
172
b, err := mgr.LoadBundle(ctx, meta.BundleNumber)
173
if err != nil {
···
175
continue
176
}
177
178
+
// Determine starting position in this bundle
179
+
startPos := 0
180
+
if i == startBundleIdx {
181
+
startPos = startPosition
182
+
}
183
+
184
+
// Send operations from this bundle
185
+
for j := startPos; j < len(b.Operations); j++ {
186
+
op := b.Operations[j]
187
188
// Send raw JSON
189
if err := sendOperation(conn, op); err != nil {
···
201
}
202
}
203
204
+
// Stream mempool operations after all bundles
205
+
streamMempool(conn, mgr, cursor, currentRecord)
206
+
}
207
+
208
+
// streamMempool streams mempool operations if cursor is in range
209
+
func streamMempool(conn *websocket.Conn, mgr *bundle.Manager, cursor int, currentRecord int) {
210
mempoolOps, err := mgr.GetMempoolOperations()
211
if err != nil {
212
fmt.Fprintf(os.Stderr, "Failed to get mempool operations: %v\n", err)
+1
-1
plc/plc_test.go
···
244
}
245
246
logger := &benchLogger{}
247
-
operations, _ := bundle.NewOperations(bundle.CompressionBetter, logger)
248
defer operations.Close()
249
250
b.ResetTimer()
···
244
}
245
246
logger := &benchLogger{}
247
+
operations, _ := bundle.NewOperations(logger)
248
defer operations.Close()
249
250
b.ResetTimer()
-7
plc_bundles.json
···
1
-
{
2
-
"version": "1.0",
3
-
"last_bundle": 0,
4
-
"updated_at": "2025-10-28T02:44:11.775384Z",
5
-
"total_size_bytes": 0,
6
-
"bundles": []
7
-
}
···
0
0
0
0
0
0
0
-6
plcbundle.go
···
16
Index = bundle.Index
17
Manager = bundle.Manager
18
Config = bundle.Config
19
-
CompressionLevel = bundle.CompressionLevel
20
VerificationResult = bundle.VerificationResult
21
ChainVerificationResult = bundle.ChainVerificationResult
22
DirectoryScanResult = bundle.DirectoryScanResult
···
31
const (
32
BUNDLE_SIZE = bundle.BUNDLE_SIZE
33
INDEX_FILE = bundle.INDEX_FILE
34
-
35
-
CompressionFastest = bundle.CompressionFastest
36
-
CompressionDefault = bundle.CompressionDefault
37
-
CompressionBetter = bundle.CompressionBetter
38
-
CompressionBest = bundle.CompressionBest
39
)
40
41
// NewManager creates a new bundle manager (convenience wrapper)
···
16
Index = bundle.Index
17
Manager = bundle.Manager
18
Config = bundle.Config
0
19
VerificationResult = bundle.VerificationResult
20
ChainVerificationResult = bundle.ChainVerificationResult
21
DirectoryScanResult = bundle.DirectoryScanResult
···
30
const (
31
BUNDLE_SIZE = bundle.BUNDLE_SIZE
32
INDEX_FILE = bundle.INDEX_FILE
0
0
0
0
0
33
)
34
35
// NewManager creates a new bundle manager (convenience wrapper)