update

Changed files
+174 -4
internal
+35
internal/api/handlers.go
··· 333 333 "scanned_at": scan.ScannedAt, 334 334 } 335 335 336 + if scan.Status != storage.EndpointStatusOnline && scan.ScanData != nil && scan.ScanData.Metadata != nil { 337 + if errorMsg, ok := scan.ScanData.Metadata["error"].(string); ok && errorMsg != "" { 338 + scanMap["error"] = errorMsg 339 + } 340 + } 341 + 336 342 if scan.ResponseTime > 0 { 337 343 scanMap["response_time"] = scan.ResponseTime 338 344 } ··· 1637 1643 } 1638 1644 1639 1645 resp.json(result) 1646 + } 1647 + 1648 + // ===== DEBUG HANDLERS ===== 1649 + 1650 + func (s *Server) handleGetDBSizes(w http.ResponseWriter, r *http.Request) { 1651 + resp := newResponse(w) 1652 + ctx := r.Context() 1653 + schema := "public" // Or make configurable if needed 1654 + 1655 + tableSizes, err := s.db.GetTableSizes(ctx, schema) 1656 + if err != nil { 1657 + log.Error("Failed to get table sizes: %v", err) 1658 + resp.error("Failed to retrieve table sizes", http.StatusInternalServerError) 1659 + return 1660 + } 1661 + 1662 + indexSizes, err := s.db.GetIndexSizes(ctx, schema) 1663 + if err != nil { 1664 + log.Error("Failed to get index sizes: %v", err) 1665 + resp.error("Failed to retrieve index sizes", http.StatusInternalServerError) 1666 + return 1667 + } 1668 + 1669 + resp.json(map[string]interface{}{ 1670 + "schema": schema, 1671 + "tables": tableSizes, 1672 + "indexes": indexSizes, 1673 + "retrievedAt": time.Now().UTC(), 1674 + }) 1640 1675 } 1641 1676 1642 1677 // ===== UTILITY FUNCTIONS =====
+2 -1
internal/api/server.go
··· 105 105 // Metrics routes 106 106 api.HandleFunc("/metrics/plc", s.handleGetPLCMetrics).Methods("GET") 107 107 108 - // Job status endpoint 108 + // Debug Endpoints 109 + api.HandleFunc("/debug/db/sizes", s.handleGetDBSizes).Methods("GET") 109 110 api.HandleFunc("/jobs", s.handleGetJobStatus).Methods("GET") 110 111 111 112 // Health check
+4
internal/storage/db.go
··· 87 87 GetPDSRepos(ctx context.Context, endpointID int64, activeOnly bool, limit int, offset int) ([]*PDSRepo, error) // Updated 88 88 GetReposByDID(ctx context.Context, did string) ([]*PDSRepo, error) 89 89 GetPDSRepoStats(ctx context.Context, endpointID int64) (map[string]interface{}, error) 90 + 91 + // Internal 92 + GetTableSizes(ctx context.Context, schema string) ([]TableSizeInfo, error) 93 + GetIndexSizes(ctx context.Context, schema string) ([]IndexSizeInfo, error) 90 94 }
+98
internal/storage/postgres.go
··· 2337 2337 "recent_changes": recentChanges, 2338 2338 }, nil 2339 2339 } 2340 + 2341 + // GetTableSizes fetches size information (in bytes) for all tables in the specified schema. 2342 + func (p *PostgresDB) GetTableSizes(ctx context.Context, schema string) ([]TableSizeInfo, error) { 2343 + // Query now selects raw byte values directly 2344 + query := ` 2345 + SELECT 2346 + c.relname AS table_name, 2347 + pg_total_relation_size(c.oid) AS total_bytes, 2348 + pg_relation_size(c.oid) AS table_heap_bytes, 2349 + pg_indexes_size(c.oid) AS indexes_bytes 2350 + FROM 2351 + pg_class c 2352 + LEFT JOIN 2353 + pg_namespace n ON n.oid = c.relnamespace 2354 + WHERE 2355 + c.relkind = 'r' -- 'r' = ordinary table 2356 + AND n.nspname = $1 2357 + ORDER BY 2358 + total_bytes DESC; 2359 + ` 2360 + rows, err := p.db.QueryContext(ctx, query, schema) 2361 + if err != nil { 2362 + return nil, fmt.Errorf("failed to query table sizes: %w", err) 2363 + } 2364 + defer rows.Close() 2365 + 2366 + var results []TableSizeInfo 2367 + for rows.Next() { 2368 + var info TableSizeInfo 2369 + // Scan directly into int64 fields 2370 + if err := rows.Scan( 2371 + &info.TableName, 2372 + &info.TotalBytes, 2373 + &info.TableHeapBytes, 2374 + &info.IndexesBytes, 2375 + ); err != nil { 2376 + return nil, fmt.Errorf("failed to scan table size row: %w", err) 2377 + } 2378 + results = append(results, info) 2379 + } 2380 + if err := rows.Err(); err != nil { 2381 + return nil, fmt.Errorf("error iterating table size rows: %w", err) 2382 + } 2383 + 2384 + return results, nil 2385 + } 2386 + 2387 + // GetIndexSizes fetches size information (in bytes) for all indexes in the specified schema. 2388 + func (p *PostgresDB) GetIndexSizes(ctx context.Context, schema string) ([]IndexSizeInfo, error) { 2389 + // Query now selects raw byte values directly 2390 + query := ` 2391 + SELECT 2392 + c.relname AS index_name, 2393 + COALESCE(i.indrelid::regclass::text, 'N/A') AS table_name, 2394 + pg_relation_size(c.oid) AS index_bytes 2395 + FROM 2396 + pg_class c 2397 + LEFT JOIN 2398 + pg_index i ON i.indexrelid = c.oid 2399 + LEFT JOIN 2400 + pg_namespace n ON n.oid = c.relnamespace 2401 + WHERE 2402 + c.relkind = 'i' -- 'i' = index 2403 + AND n.nspname = $1 2404 + ORDER BY 2405 + index_bytes DESC; 2406 + ` 2407 + rows, err := p.db.QueryContext(ctx, query, schema) 2408 + if err != nil { 2409 + return nil, fmt.Errorf("failed to query index sizes: %w", err) 2410 + } 2411 + defer rows.Close() 2412 + 2413 + var results []IndexSizeInfo 2414 + for rows.Next() { 2415 + var info IndexSizeInfo 2416 + var tableName sql.NullString 2417 + // Scan directly into int64 field 2418 + if err := rows.Scan( 2419 + &info.IndexName, 2420 + &tableName, 2421 + &info.IndexBytes, 2422 + ); err != nil { 2423 + return nil, fmt.Errorf("failed to scan index size row: %w", err) 2424 + } 2425 + if tableName.Valid { 2426 + info.TableName = tableName.String 2427 + } else { 2428 + info.TableName = "N/A" 2429 + } 2430 + results = append(results, info) 2431 + } 2432 + if err := rows.Err(); err != nil { 2433 + return nil, fmt.Errorf("error iterating index size rows: %w", err) 2434 + } 2435 + 2436 + return results, nil 2437 + }
+35 -3
internal/storage/types.go
··· 1 1 package storage 2 2 3 3 import ( 4 + "database/sql" 4 5 "fmt" 5 6 "path/filepath" 6 7 "time" ··· 170 171 // DIDRecord represents a DID entry in the database 171 172 type DIDRecord struct { 172 173 DID string `json:"did"` 174 + Handle string `json:"handle,omitempty"` 175 + CurrentPDS string `json:"current_pds,omitempty"` 176 + LastOpAt time.Time `json:"last_op_at,omitempty"` 173 177 BundleNumbers []int `json:"bundle_numbers"` 174 178 CreatedAt time.Time `json:"created_at"` 179 + } 180 + 181 + // GlobalDIDInfo consolidates DID data from PLC and PDS tables 182 + type GlobalDIDInfo struct { 183 + DIDRecord // Embeds all fields: DID, Handle, CurrentPDS, etc. 184 + HostingOn []*PDSRepo `json:"hosting_on"` 175 185 } 176 186 177 187 // IPInfo represents IP information (stored with IP as primary key) ··· 275 285 Status string 276 286 } 277 287 278 - type GlobalDIDInfo struct { 279 - DIDRecord 280 - HostingOn []*PDSRepo `json:"hosting_on"` 288 + type DIDBackfillInfo struct { 289 + DID string 290 + LastBundleNum int 291 + } 292 + 293 + type DIDStateUpdateData struct { 294 + DID string 295 + Handle sql.NullString // Use sql.NullString for potential NULLs 296 + PDS sql.NullString 297 + OpTime time.Time 298 + } 299 + 300 + // TableSizeInfo holds size information for a database table. 301 + type TableSizeInfo struct { 302 + TableName string `json:"table_name"` 303 + TotalBytes int64 `json:"total_bytes"` // Raw bytes 304 + TableHeapBytes int64 `json:"table_heap_bytes"` // Raw bytes 305 + IndexesBytes int64 `json:"indexes_bytes"` // Raw bytes 306 + } 307 + 308 + // IndexSizeInfo holds size information for a database index. 309 + type IndexSizeInfo struct { 310 + IndexName string `json:"index_name"` 311 + TableName string `json:"table_name"` 312 + IndexBytes int64 `json:"index_bytes"` // Raw bytes 281 313 }