update

Changed files
+126
internal
+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
··· 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
··· 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
··· 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
··· 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