this repo has no description coral.waow.tech

add persistence, graceful shutdown, and UI refresh

backend:
- init SQLite + load/save entity graph state
- SIGTERM handler for graceful shutdown on fly.io
- periodic saver thread (every 30s)

site:
- updated copy (turbostream-powered, clusters)
- simplified stats display

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+113 -56
+7
backend/build.zig
··· 9 9 .optimize = optimize, 10 10 }); 11 11 12 + const zqlite = b.dependency("zqlite", .{ 13 + .target = target, 14 + .optimize = optimize, 15 + }); 16 + 12 17 const exe = b.addExecutable(.{ 13 18 .name = "coral", 14 19 .root_module = b.createModule(.{ ··· 17 22 .optimize = optimize, 18 23 .imports = &.{ 19 24 .{ .name = "websocket", .module = websocket.module("websocket") }, 25 + .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 20 26 }, 21 27 }), 22 28 }); ··· 42 48 .optimize = optimize, 43 49 .imports = &.{ 44 50 .{ .name = "websocket", .module = websocket.module("websocket") }, 51 + .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 45 52 }, 46 53 }), 47 54 });
+4
backend/build.zig.zon
··· 8 8 .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 9 9 .hash = "websocket-0.1.0-ZPISdRNzAwAGszh62EpRtoQxu8wb1MSMVI6Ow0o2dmyJ", 10 10 }, 11 + .zqlite = .{ 12 + .url = "https://github.com/karlseguin/zqlite.zig/archive/refs/heads/master.tar.gz", 13 + .hash = "zqlite-0.0.0-RWLaY_y_mADh2LdbDrG_2HT2dBAcsAR8Jig_7-dOJd0B", 14 + }, 11 15 }, 12 16 .paths = .{ 13 17 "build.zig",
+9
backend/fly.toml
··· 22 22 internal_port = 3001 23 23 protocol = "tcp" 24 24 25 + [services.concurrency] 26 + type = "connections" 27 + hard_limit = 1000 28 + soft_limit = 500 29 + 25 30 [[services.ports]] 26 31 port = 3001 27 32 handlers = ["tls"] 33 + 34 + [mounts] 35 + source = "coral_data" 36 + destination = "/data" 28 37 29 38 [[vm]] 30 39 memory = '256mb'
+79
backend/src/main.zig
··· 6 6 const lattice = @import("lattice.zig"); 7 7 const http = @import("http.zig"); 8 8 const ws_server = @import("ws_server.zig"); 9 + const entity_graph = @import("entity_graph.zig"); 10 + const db = @import("db.zig"); 9 11 10 12 const log = std.log.scoped(.main); 11 13 12 14 const MAX_HTTP_WORKERS = 8; 13 15 const SOCKET_TIMEOUT_SECS = 30; 16 + const SAVE_INTERVAL_MS = 30_000; // save every 30 seconds 17 + const DB_PATH = "/data/coral.db"; 18 + 19 + // shutdown flag for graceful termination 20 + var shutdown_requested = std.atomic.Value(bool).init(false); 14 21 15 22 pub fn main() !void { 16 23 var gpa = std.heap.GeneralPurposeAllocator(.{ ··· 21 28 22 29 // init world with three lattices (32, 128, 512) 23 30 lattice.init(); 31 + 32 + // init entity graph config from env vars 33 + entity_graph.initConfig(); 34 + 35 + // init SQLite database for persistence 36 + db.init(DB_PATH) catch |err| { 37 + log.warn("failed to init database: {}, state won't persist", .{err}); 38 + }; 39 + 40 + // load persisted graph state from SQLite 41 + db.loadState(&entity_graph.graph) catch |err| { 42 + log.warn("failed to load state from db: {}, starting fresh", .{err}); 43 + }; 44 + 45 + // setup SIGTERM handler for graceful shutdown 46 + const sigterm_action = posix.Sigaction{ 47 + .handler = .{ .handler = handleSigterm }, 48 + .mask = posix.sigemptyset(), 49 + .flags = 0, 50 + }; 51 + posix.sigaction(posix.SIG.TERM, &sigterm_action, null); 52 + 53 + // spawn periodic saver thread 54 + const saver_thread = try Thread.spawn(.{}, periodicSaver, .{}); 55 + saver_thread.detach(); 24 56 25 57 // init websocket client tracking 26 58 ws_server.init(allocator); ··· 83 115 try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 84 116 } 85 117 118 + fn handleSigterm(_: c_int) callconv(.c) void { 119 + log.info("received SIGTERM, initiating graceful shutdown", .{}); 120 + shutdown_requested.store(true, .release); 121 + } 122 + 123 + fn periodicSaver() void { 124 + while (!shutdown_requested.load(.acquire)) { 125 + // check shutdown every second, save every SAVE_INTERVAL_MS 126 + var elapsed_ms: u64 = 0; 127 + while (elapsed_ms < SAVE_INTERVAL_MS and !shutdown_requested.load(.acquire)) { 128 + Thread.sleep(1000 * std.time.ns_per_ms); // 1 second 129 + elapsed_ms += 1000; 130 + } 131 + 132 + // save state to SQLite 133 + entity_graph.graph.mutex.lock(); 134 + db.saveState(&entity_graph.graph) catch |err| { 135 + log.err("failed to save state: {}", .{err}); 136 + }; 137 + entity_graph.graph.mutex.unlock(); 138 + 139 + // prune posts: keep top N per entity, delete orphans 140 + db.pruneEntityPosts() catch |err| { 141 + log.warn("failed to prune entity posts: {}", .{err}); 142 + }; 143 + db.pruneOrphanPosts() catch |err| { 144 + log.warn("failed to prune orphan posts: {}", .{err}); 145 + }; 146 + 147 + if (shutdown_requested.load(.acquire)) { 148 + log.info("graceful shutdown: state saved, exiting", .{}); 149 + posix.exit(0); 150 + } 151 + } 152 + 153 + // final save on shutdown 154 + log.info("shutdown requested, saving final state", .{}); 155 + entity_graph.graph.mutex.lock(); 156 + db.saveState(&entity_graph.graph) catch |err| { 157 + log.err("failed to save final state: {}", .{err}); 158 + }; 159 + entity_graph.graph.mutex.unlock(); 160 + log.info("graceful shutdown complete", .{}); 161 + posix.exit(0); 162 + } 163 + 86 164 test { 87 165 _ = @import("lattice.zig"); 88 166 _ = @import("http.zig"); 89 167 _ = @import("ws_server.zig"); 90 168 _ = @import("entities.zig"); 169 + _ = @import("entity_graph.zig"); 91 170 }
+14 -56
site/index.html
··· 7 7 8 8 <!-- open graph --> 9 9 <meta property="og:title" content="coral"> 10 - <meta property="og:description" content="named entity percolation from the Bluesky firehose"> 10 + <meta property="og:description" content="real-time entity visualization powered by turbostream"> 11 11 <meta property="og:image" content="https://coral-8hh.pages.dev/og.png"> 12 12 <meta property="og:url" content="https://coral-8hh.pages.dev"> 13 13 <meta property="og:type" content="website"> ··· 15 15 <!-- twitter --> 16 16 <meta name="twitter:card" content="summary_large_image"> 17 17 <meta name="twitter:title" content="coral"> 18 - <meta name="twitter:description" content="named entity percolation from the Bluesky firehose"> 18 + <meta name="twitter:description" content="real-time entity visualization powered by turbostream"> 19 19 <meta name="twitter:image" content="https://coral-8hh.pages.dev/og.png"> 20 20 21 21 <link rel="icon" type="image/svg+xml" href="favicon.svg"> ··· 38 38 39 39 <div class="info"> 40 40 <p class="description"> 41 - Named entities (people, places, orgs) extracted from Bluesky posts 42 - hash to grid positions. Near <strong>~59%</strong> density, isolated 43 - clusters merge into a spanning network &mdash; a phase transition. 41 + Named entities extracted in real-time, powered by <a href="https://graze.social/docs/graze-turbostream" target="_blank">turbostream</a>. 42 + Co-occurring entities form <strong>clusters</strong> as conversations develop. 44 43 </p> 45 44 46 - <div class="density-meter"> 47 - <div class="density-bar"> 48 - <div class="density-fill" id="density-fill"></div> 49 - <div class="density-critical"></div> 50 - </div> 51 - <div class="density-labels"> 52 - <span>0%</span> 53 - <span class="critical-label">59%</span> 54 - <span>100%</span> 55 - </div> 56 - </div> 57 - 58 - <div class="scaling-curve" title="universal scaling function: (p - 0.593) × L^(3/4)"> 59 - <canvas id="scaling-canvas" width="260" height="80"></canvas> 60 - <div class="scaling-label"> 61 - <span>x = <span id="scaling-x">--</span></span> 62 - </div> 63 - </div> 64 - 65 - <div class="status-row"> 66 - <div class="big-stat" title="does a cluster connect top to bottom?"> 67 - <span class="big-value" id="spanning">--</span> 68 - <span class="big-label">spanning</span> 69 - </div> 70 - <div class="big-stat" title="sites in the largest connected cluster"> 71 - <span class="big-value" id="largest">--</span> 72 - <span class="big-label">largest cluster</span> 45 + <div class="stats-row"> 46 + <div class="stat" title="active entities on grid"> 47 + <span class="stat-value" id="active-count">--</span> 48 + <span class="stat-label">active</span> 73 49 </div> 74 - </div> 75 - 76 - <div class="mini-stats"> 77 - <div class="mini-stat" title="named entities extracted from posts"> 78 - <span class="mini-value" id="posts">--</span> 79 - <span class="mini-label">entities</span> 50 + <div class="stat" title="connected clusters"> 51 + <span class="stat-value" id="clusters">--</span> 52 + <span class="stat-label">clusters</span> 80 53 </div> 81 - <div class="mini-stat" title="number of isolated connected components"> 82 - <span class="mini-value" id="clusters">--</span> 83 - <span class="mini-label">clusters</span> 84 - </div> 85 - <div class="mini-stat" title="times the system crossed the percolation threshold"> 86 - <span class="mini-value" id="crossings">--</span> 87 - <span class="mini-label">crossings</span> 54 + <div class="stat" title="entities in largest cluster"> 55 + <span class="stat-value" id="largest">--</span> 56 + <span class="stat-label">largest</span> 88 57 </div> 89 58 </div> 90 59 ··· 97 66 <div class="section-label">live</div> 98 67 <div class="live-feed" id="live-feed"></div> 99 68 </div> 100 - </div> 101 - </div> 102 - 103 - <div class="mini-grids"> 104 - <div class="mini-grid"> 105 - <canvas id="grid-32" data-size="32"></canvas> 106 - <span>32x32 (70%)</span> 107 - </div> 108 - <div class="mini-grid"> 109 - <canvas id="grid-512" data-size="512"></canvas> 110 - <span>512x512 (40%)</span> 111 69 </div> 112 70 </div> 113 71