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 .optimize = optimize, 10 }); 11 12 const exe = b.addExecutable(.{ 13 .name = "coral", 14 .root_module = b.createModule(.{ ··· 17 .optimize = optimize, 18 .imports = &.{ 19 .{ .name = "websocket", .module = websocket.module("websocket") }, 20 }, 21 }), 22 }); ··· 42 .optimize = optimize, 43 .imports = &.{ 44 .{ .name = "websocket", .module = websocket.module("websocket") }, 45 }, 46 }), 47 });
··· 9 .optimize = optimize, 10 }); 11 12 + const zqlite = b.dependency("zqlite", .{ 13 + .target = target, 14 + .optimize = optimize, 15 + }); 16 + 17 const exe = b.addExecutable(.{ 18 .name = "coral", 19 .root_module = b.createModule(.{ ··· 22 .optimize = optimize, 23 .imports = &.{ 24 .{ .name = "websocket", .module = websocket.module("websocket") }, 25 + .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 26 }, 27 }), 28 }); ··· 48 .optimize = optimize, 49 .imports = &.{ 50 .{ .name = "websocket", .module = websocket.module("websocket") }, 51 + .{ .name = "zqlite", .module = zqlite.module("zqlite") }, 52 }, 53 }), 54 });
+4
backend/build.zig.zon
··· 8 .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 9 .hash = "websocket-0.1.0-ZPISdRNzAwAGszh62EpRtoQxu8wb1MSMVI6Ow0o2dmyJ", 10 }, 11 }, 12 .paths = .{ 13 "build.zig",
··· 8 .url = "https://github.com/karlseguin/websocket.zig/archive/refs/heads/master.tar.gz", 9 .hash = "websocket-0.1.0-ZPISdRNzAwAGszh62EpRtoQxu8wb1MSMVI6Ow0o2dmyJ", 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 + }, 15 }, 16 .paths = .{ 17 "build.zig",
+9
backend/fly.toml
··· 22 internal_port = 3001 23 protocol = "tcp" 24 25 [[services.ports]] 26 port = 3001 27 handlers = ["tls"] 28 29 [[vm]] 30 memory = '256mb'
··· 22 internal_port = 3001 23 protocol = "tcp" 24 25 + [services.concurrency] 26 + type = "connections" 27 + hard_limit = 1000 28 + soft_limit = 500 29 + 30 [[services.ports]] 31 port = 3001 32 handlers = ["tls"] 33 + 34 + [mounts] 35 + source = "coral_data" 36 + destination = "/data" 37 38 [[vm]] 39 memory = '256mb'
+79
backend/src/main.zig
··· 6 const lattice = @import("lattice.zig"); 7 const http = @import("http.zig"); 8 const ws_server = @import("ws_server.zig"); 9 10 const log = std.log.scoped(.main); 11 12 const MAX_HTTP_WORKERS = 8; 13 const SOCKET_TIMEOUT_SECS = 30; 14 15 pub fn main() !void { 16 var gpa = std.heap.GeneralPurposeAllocator(.{ ··· 21 22 // init world with three lattices (32, 128, 512) 23 lattice.init(); 24 25 // init websocket client tracking 26 ws_server.init(allocator); ··· 83 try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 84 } 85 86 test { 87 _ = @import("lattice.zig"); 88 _ = @import("http.zig"); 89 _ = @import("ws_server.zig"); 90 _ = @import("entities.zig"); 91 }
··· 6 const lattice = @import("lattice.zig"); 7 const http = @import("http.zig"); 8 const ws_server = @import("ws_server.zig"); 9 + const entity_graph = @import("entity_graph.zig"); 10 + const db = @import("db.zig"); 11 12 const log = std.log.scoped(.main); 13 14 const MAX_HTTP_WORKERS = 8; 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); 21 22 pub fn main() !void { 23 var gpa = std.heap.GeneralPurposeAllocator(.{ ··· 28 29 // init world with three lattices (32, 128, 512) 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(); 56 57 // init websocket client tracking 58 ws_server.init(allocator); ··· 115 try posix.setsockopt(fd, posix.SOL.SOCKET, posix.SO.SNDTIMEO, &timeout); 116 } 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 + 164 test { 165 _ = @import("lattice.zig"); 166 _ = @import("http.zig"); 167 _ = @import("ws_server.zig"); 168 _ = @import("entities.zig"); 169 + _ = @import("entity_graph.zig"); 170 }
+14 -56
site/index.html
··· 7 8 <!-- open graph --> 9 <meta property="og:title" content="coral"> 10 - <meta property="og:description" content="named entity percolation from the Bluesky firehose"> 11 <meta property="og:image" content="https://coral-8hh.pages.dev/og.png"> 12 <meta property="og:url" content="https://coral-8hh.pages.dev"> 13 <meta property="og:type" content="website"> ··· 15 <!-- twitter --> 16 <meta name="twitter:card" content="summary_large_image"> 17 <meta name="twitter:title" content="coral"> 18 - <meta name="twitter:description" content="named entity percolation from the Bluesky firehose"> 19 <meta name="twitter:image" content="https://coral-8hh.pages.dev/og.png"> 20 21 <link rel="icon" type="image/svg+xml" href="favicon.svg"> ··· 38 39 <div class="info"> 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. 44 </p> 45 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> 73 </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> 80 </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> 88 </div> 89 </div> 90 ··· 97 <div class="section-label">live</div> 98 <div class="live-feed" id="live-feed"></div> 99 </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 </div> 112 </div> 113
··· 7 8 <!-- open graph --> 9 <meta property="og:title" content="coral"> 10 + <meta property="og:description" content="real-time entity visualization powered by turbostream"> 11 <meta property="og:image" content="https://coral-8hh.pages.dev/og.png"> 12 <meta property="og:url" content="https://coral-8hh.pages.dev"> 13 <meta property="og:type" content="website"> ··· 15 <!-- twitter --> 16 <meta name="twitter:card" content="summary_large_image"> 17 <meta name="twitter:title" content="coral"> 18 + <meta name="twitter:description" content="real-time entity visualization powered by turbostream"> 19 <meta name="twitter:image" content="https://coral-8hh.pages.dev/og.png"> 20 21 <link rel="icon" type="image/svg+xml" href="favicon.svg"> ··· 38 39 <div class="info"> 40 <p class="description"> 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. 43 </p> 44 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> 49 </div> 50 + <div class="stat" title="connected clusters"> 51 + <span class="stat-value" id="clusters">--</span> 52 + <span class="stat-label">clusters</span> 53 </div> 54 + <div class="stat" title="entities in largest cluster"> 55 + <span class="stat-value" id="largest">--</span> 56 + <span class="stat-label">largest</span> 57 </div> 58 </div> 59 ··· 66 <div class="section-label">live</div> 67 <div class="live-feed" id="live-feed"></div> 68 </div> 69 </div> 70 </div> 71