A Transparent and Verifiable Way to Sync the AT Protocol's PLC Directory

sync status as json

+31 -61
+31 -61
cmd/plcbundle/server.go
··· 170 170 fmt.Fprintf(w, "plcbundle v%s | https://github.com/atscan/plcbundle\n", version) 171 171 } 172 172 173 - // handleSync returns sync status and mempool info 173 + // handleSync returns sync status and mempool info as JSON 174 174 func handleSync(w http.ResponseWriter, mgr *bundle.Manager) { 175 - w.Header().Set("Content-Type", "text/plain; charset=utf-8") 175 + w.Header().Set("Content-Type", "application/json") 176 176 w.Header().Set("Access-Control-Allow-Origin", "*") 177 177 178 178 index := mgr.GetIndex() 179 - stats := index.GetStats() 180 - bundleCount := stats["bundle_count"].(int) 179 + indexStats := index.GetStats() 180 + mempoolStats := mgr.GetMempoolStats() 181 181 182 - fmt.Fprintf(w, "Sync Status\n") 183 - fmt.Fprintf(w, "═══════════\n\n") 184 - 185 - // Bundle info 186 - fmt.Fprintf(w, "Bundles\n") 187 - fmt.Fprintf(w, "───────\n") 188 - fmt.Fprintf(w, "Count: %d\n", bundleCount) 189 - 190 - if bundleCount > 0 { 191 - firstBundle := stats["first_bundle"].(int) 192 - lastBundle := stats["last_bundle"].(int) 193 - totalSize := stats["total_size"].(int64) 182 + // Build response 183 + response := map[string]interface{}{ 184 + "bundles": map[string]interface{}{ 185 + "count": indexStats["bundle_count"], 186 + "total_size": indexStats["total_size"], 187 + "updated_at": indexStats["updated_at"], 188 + }, 189 + "mempool": mempoolStats, 190 + } 194 191 195 - fmt.Fprintf(w, "Range: %06d - %06d\n", firstBundle, lastBundle) 196 - fmt.Fprintf(w, "Total size: %.2f MB\n", float64(totalSize)/(1024*1024)) 197 - fmt.Fprintf(w, "Last updated: %s\n", stats["updated_at"].(time.Time).Format(time.RFC3339)) 192 + // Add bundle range if bundles exist 193 + if bundleCount, ok := indexStats["bundle_count"].(int); ok && bundleCount > 0 { 194 + response["bundles"].(map[string]interface{})["first_bundle"] = indexStats["first_bundle"] 195 + response["bundles"].(map[string]interface{})["last_bundle"] = indexStats["last_bundle"] 196 + response["bundles"].(map[string]interface{})["start_time"] = indexStats["start_time"] 197 + response["bundles"].(map[string]interface{})["end_time"] = indexStats["end_time"] 198 198 199 - if gaps, ok := stats["gaps"].(int); ok && gaps > 0 { 200 - fmt.Fprintf(w, "⚠ Gaps: %d missing bundles\n", gaps) 199 + if gaps, ok := indexStats["gaps"].(int); ok { 200 + response["bundles"].(map[string]interface{})["gaps"] = gaps 201 201 } 202 202 } 203 203 204 - // Mempool info 205 - mempoolStats := mgr.GetMempoolStats() 206 - count := mempoolStats["count"].(int) 207 - targetBundle := mempoolStats["target_bundle"].(int) 208 - canCreate := mempoolStats["can_create_bundle"].(bool) 209 - minTimestamp := mempoolStats["min_timestamp"].(time.Time) 210 - validated := mempoolStats["validated"].(bool) 211 - 212 - fmt.Fprintf(w, "\nMempool\n") 213 - fmt.Fprintf(w, "───────\n") 214 - fmt.Fprintf(w, "Target bundle: %06d\n", targetBundle) 215 - fmt.Fprintf(w, "Operations: %d / %d\n", count, bundle.BUNDLE_SIZE) 216 - fmt.Fprintf(w, "Can create bundle: %v\n", canCreate) 217 - fmt.Fprintf(w, "Min timestamp: %s\n", minTimestamp.Format(time.RFC3339)) 218 - fmt.Fprintf(w, "Validated: %v\n", validated) 219 - 220 - if count > 0 { 204 + // Calculate mempool progress percentage 205 + if count, ok := mempoolStats["count"].(int); ok { 221 206 progress := float64(count) / float64(bundle.BUNDLE_SIZE) * 100 222 - fmt.Fprintf(w, "Progress: %.1f%%\n", progress) 223 - 224 - // Progress bar 225 - barWidth := 50 226 - filled := int(float64(barWidth) * float64(count) / float64(bundle.BUNDLE_SIZE)) 227 - if filled > barWidth { 228 - filled = barWidth 229 - } 230 - bar := strings.Repeat("█", filled) + strings.Repeat("░", barWidth-filled) 231 - fmt.Fprintf(w, "\n[%s]\n\n", bar) 207 + response["mempool"].(map[string]interface{})["progress_percent"] = progress 208 + response["mempool"].(map[string]interface{})["bundle_size"] = bundle.BUNDLE_SIZE 209 + } 232 210 233 - if sizeBytes, ok := mempoolStats["size_bytes"].(int); ok { 234 - fmt.Fprintf(w, "Size: %.2f KB\n", float64(sizeBytes)/1024) 235 - } 236 - if firstTime, ok := mempoolStats["first_time"].(time.Time); ok { 237 - fmt.Fprintf(w, "First operation: %s\n", firstTime.Format(time.RFC3339)) 238 - } 239 - if lastTime, ok := mempoolStats["last_time"].(time.Time); ok { 240 - fmt.Fprintf(w, "Last operation: %s\n", lastTime.Format(time.RFC3339)) 241 - } 242 - } else { 243 - fmt.Fprintf(w, "\n(empty)\n") 211 + data, err := json.MarshalIndent(response, "", " ") 212 + if err != nil { 213 + http.Error(w, "Failed to marshal sync status", http.StatusInternalServerError) 214 + return 244 215 } 245 216 246 - fmt.Fprintf(w, "\nEndpoints:\n") 247 - fmt.Fprintf(w, " /sync/mempool - JSONL stream of mempool operations\n") 217 + w.Write(data) 248 218 } 249 219 250 220 // handleSyncMempool streams mempool operations as JSONL