+49
internal/api/handlers.go
+49
internal/api/handlers.go
···
1398
1398
})
1399
1399
}
1400
1400
1401
+
func (s *Server) handleGetPLCHistory(w http.ResponseWriter, r *http.Request) {
1402
+
resp := newResponse(w)
1403
+
1404
+
limit := getQueryInt(r, "limit", 0)
1405
+
fromBundle := getQueryInt(r, "from", 1)
1406
+
1407
+
history, err := s.db.GetPLCHistory(r.Context(), limit, fromBundle)
1408
+
if err != nil {
1409
+
resp.error(err.Error(), http.StatusInternalServerError)
1410
+
return
1411
+
}
1412
+
1413
+
var totalOps int64
1414
+
var totalUncompressed int64
1415
+
var totalCompressed int64
1416
+
1417
+
for _, point := range history {
1418
+
totalOps += int64(point.OperationCount)
1419
+
totalUncompressed += point.UncompressedSize
1420
+
totalCompressed += point.CompressedSize
1421
+
}
1422
+
1423
+
result := map[string]interface{}{
1424
+
"data": history,
1425
+
"summary": map[string]interface{}{
1426
+
"days": len(history),
1427
+
"total_operations": totalOps,
1428
+
"total_uncompressed": totalUncompressed,
1429
+
"total_compressed": totalCompressed,
1430
+
"compression_ratio": 0.0,
1431
+
},
1432
+
}
1433
+
1434
+
if len(history) > 0 {
1435
+
result["summary"].(map[string]interface{})["first_date"] = history[0].Date
1436
+
result["summary"].(map[string]interface{})["last_date"] = history[len(history)-1].Date
1437
+
result["summary"].(map[string]interface{})["time_span_days"] = len(history)
1438
+
1439
+
if totalCompressed > 0 {
1440
+
result["summary"].(map[string]interface{})["compression_ratio"] = float64(totalUncompressed) / float64(totalCompressed)
1441
+
}
1442
+
1443
+
result["summary"].(map[string]interface{})["avg_operations_per_day"] = totalOps / int64(len(history))
1444
+
result["summary"].(map[string]interface{})["avg_size_per_day"] = totalUncompressed / int64(len(history))
1445
+
}
1446
+
1447
+
resp.json(result)
1448
+
}
1449
+
1401
1450
// ===== UTILITY FUNCTIONS =====
1402
1451
1403
1452
func computeOperationsHash(ops []plc.PLCOperation) string {
+3
internal/api/server.go
+3
internal/api/server.go
···
78
78
api.HandleFunc("/plc/bundles/{number}/download", s.handleDownloadPLCBundle).Methods("GET")
79
79
api.HandleFunc("/plc/bundles/{bundleNumber}/verify", s.handleVerifyPLCBundle).Methods("POST")
80
80
81
+
// PLC history/metrics
82
+
api.HandleFunc("/plc/history", s.handleGetPLCHistory).Methods("GET")
83
+
81
84
// PLC Export endpoint (simulates PLC directory)
82
85
api.HandleFunc("/plc/export", s.handlePLCExport).Methods("GET")
83
86
+1
internal/storage/db.go
+1
internal/storage/db.go
···
56
56
GetBundleStats(ctx context.Context) (count, compressedSize, uncompressedSize, lastBundle int64, err error)
57
57
GetLastBundleNumber(ctx context.Context) (int, error)
58
58
GetBundleForTimestamp(ctx context.Context, afterTime time.Time) (int, error)
59
+
GetPLCHistory(ctx context.Context, limit int, fromBundle int) ([]*PLCHistoryPoint, error)
59
60
60
61
// Mempool operations
61
62
AddToMempool(ctx context.Context, ops []MempoolOperation) error
+63
internal/storage/postgres.go
+63
internal/storage/postgres.go
···
1332
1332
return bundleNum, nil
1333
1333
}
1334
1334
1335
+
func (p *PostgresDB) GetPLCHistory(ctx context.Context, limit int, fromBundle int) ([]*PLCHistoryPoint, error) {
1336
+
query := `
1337
+
WITH daily_stats AS (
1338
+
SELECT
1339
+
DATE(start_time) as date,
1340
+
MAX(bundle_number) as last_bundle,
1341
+
COUNT(*) as bundle_count,
1342
+
SUM(uncompressed_size) as total_uncompressed,
1343
+
SUM(compressed_size) as total_compressed,
1344
+
MAX(cumulative_uncompressed_size) as cumulative_uncompressed,
1345
+
MAX(cumulative_compressed_size) as cumulative_compressed
1346
+
FROM plc_bundles
1347
+
WHERE bundle_number >= $1
1348
+
GROUP BY DATE(start_time)
1349
+
)
1350
+
SELECT
1351
+
date::text,
1352
+
last_bundle,
1353
+
SUM(bundle_count * 10000) OVER (ORDER BY date) as cumulative_operations,
1354
+
total_uncompressed,
1355
+
total_compressed,
1356
+
cumulative_uncompressed,
1357
+
cumulative_compressed
1358
+
FROM daily_stats
1359
+
ORDER BY date ASC
1360
+
`
1361
+
1362
+
if limit > 0 {
1363
+
query += fmt.Sprintf(" LIMIT %d", limit)
1364
+
}
1365
+
1366
+
rows, err := p.db.QueryContext(ctx, query, fromBundle)
1367
+
if err != nil {
1368
+
return nil, err
1369
+
}
1370
+
defer rows.Close()
1371
+
1372
+
var history []*PLCHistoryPoint
1373
+
for rows.Next() {
1374
+
var point PLCHistoryPoint
1375
+
var cumulativeOps int64
1376
+
1377
+
err := rows.Scan(
1378
+
&point.Date,
1379
+
&point.BundleNumber,
1380
+
&cumulativeOps,
1381
+
&point.UncompressedSize,
1382
+
&point.CompressedSize,
1383
+
&point.CumulativeUncompressed,
1384
+
&point.CumulativeCompressed,
1385
+
)
1386
+
if err != nil {
1387
+
return nil, err
1388
+
}
1389
+
1390
+
point.OperationCount = int(cumulativeOps)
1391
+
1392
+
history = append(history, &point)
1393
+
}
1394
+
1395
+
return history, rows.Err()
1396
+
}
1397
+
1335
1398
// ===== MEMPOOL OPERATIONS =====
1336
1399
1337
1400
func (p *PostgresDB) AddToMempool(ctx context.Context, ops []MempoolOperation) error {
+10
internal/storage/types.go
+10
internal/storage/types.go
···
139
139
return 10000
140
140
}
141
141
142
+
type PLCHistoryPoint struct {
143
+
Date string `json:"date"`
144
+
BundleNumber int `json:"last_bundle_number"`
145
+
OperationCount int `json:"operations"`
146
+
UncompressedSize int64 `json:"size_uncompressed"`
147
+
CompressedSize int64 `json:"size_compressed"`
148
+
CumulativeUncompressed int64 `json:"cumulative_uncompressed"`
149
+
CumulativeCompressed int64 `json:"cumulative_compressed"`
150
+
}
151
+
142
152
// MempoolOperation represents an operation waiting to be bundled
143
153
type MempoolOperation struct {
144
154
ID int64