The AtmosphereConf talks your skyline missed
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

chore: add mock crawl data for local preview

MOCK_CRAWL=1 env var serves data/mock-crawl.json from /api/crawl
without authentication. Generate with: npx tsx scripts/seed-mock-crawl.ts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+99
+3
.gitignore
··· 149 149 150 150 # Agent config (local) 151 151 .agents/ 152 + 153 + # Mock data for local dev 154 + data/mock-crawl.json
+84
scripts/seed-mock-crawl.ts
··· 1 + /** 2 + * Generates mock crawl data with a realistic spread of mention counts. 3 + * Output: data/mock-crawl.json 4 + * 5 + * Usage: npx tsx scripts/seed-mock-crawl.ts 6 + */ 7 + 8 + import * as fs from "fs"; 9 + import * as path from "path"; 10 + import type { TalkMention, TalkMentions } from "@/lib/crawl/types"; 11 + 12 + interface TalkEntry { 13 + rkey: string; 14 + eventUri: string | null; 15 + transcriptFile: string | null; 16 + } 17 + 18 + const DATA_DIR = path.resolve(process.cwd(), "data"); 19 + const talks: TalkEntry[] = JSON.parse( 20 + fs.readFileSync(path.join(DATA_DIR, "talks.json"), "utf-8"), 21 + ); 22 + 23 + const scoredTalks = talks.filter((t) => t.eventUri && t.transcriptFile); 24 + 25 + // Simulate 150 follows, ~25 of whom discussed any talk 26 + const TOTAL_FOLLOWS = 150; 27 + const ENGAGED_POOL = Array.from( 28 + { length: 25 }, 29 + (_, i) => `did:plc:mock${String(i).padStart(3, "0")}`, 30 + ); 31 + 32 + // Distribute mention counts with a power-law-ish spread: 33 + // - ~10% of talks: heavily discussed (8-20 engaged follows) 34 + // - ~30% of talks: moderately discussed (2-7 engaged follows) 35 + // - ~30% of talks: lightly discussed (1 engaged follow) 36 + // - ~30% of talks: completely missed (0 engaged follows) 37 + function assignMentionCount(index: number, total: number): number { 38 + const pct = index / total; 39 + if (pct < 0.1) return Math.floor(Math.random() * 13) + 8; // 8-20 40 + if (pct < 0.4) return Math.floor(Math.random() * 6) + 2; // 2-7 41 + if (pct < 0.7) return 1; 42 + return 0; 43 + } 44 + 45 + // Shuffle to randomize which talks get high/low mentions 46 + const shuffled = [...scoredTalks].sort(() => Math.random() - 0.5); 47 + 48 + const talkMentions: TalkMentions = {}; 49 + for (let i = 0; i < shuffled.length; i++) { 50 + const count = assignMentionCount(i, shuffled.length); 51 + // Pick `count` random follows from the engaged pool 52 + const pool = [...ENGAGED_POOL].sort(() => Math.random() - 0.5); 53 + const follows = pool.slice(0, count); 54 + const mention: TalkMention = { 55 + count: follows.length, 56 + follows, 57 + posts: follows.map( 58 + (did) => `at://${did}/app.bsky.feed.post/mock${Date.now()}`, 59 + ), 60 + rsvps: [], 61 + }; 62 + talkMentions[shuffled[i].rkey] = mention; 63 + } 64 + 65 + const mockCrawl = { 66 + talkMentions, 67 + followCount: TOTAL_FOLLOWS, 68 + postsScanned: 1200, 69 + crawledAt: Date.now(), 70 + }; 71 + 72 + const outPath = path.join(DATA_DIR, "mock-crawl.json"); 73 + fs.writeFileSync(outPath, JSON.stringify(mockCrawl, null, 2)); 74 + 75 + // Stats 76 + const counts = Object.values(talkMentions).map((m) => m.follows.length); 77 + const missed = counts.filter((c) => c === 0).length; 78 + const light = counts.filter((c) => c === 1).length; 79 + const moderate = counts.filter((c) => c >= 2 && c <= 7).length; 80 + const heavy = counts.filter((c) => c >= 8).length; 81 + 82 + console.log(`Wrote ${outPath}`); 83 + console.log(`${counts.length} talks: ${heavy} heavy, ${moderate} moderate, ${light} light, ${missed} missed`); 84 + console.log(`Engaged follow pool: ${ENGAGED_POOL.length} / ${TOTAL_FOLLOWS} total`);
+12
src/app/api/crawl/route.ts
··· 1 + import * as fs from "fs"; 2 + import * as path from "path"; 1 3 import { NextRequest, NextResponse } from "next/server"; 2 4 import { getSession } from "@/lib/auth/session"; 3 5 import { crawl } from "@/lib/crawl/crawler"; ··· 5 7 import type { CrawlResult } from "@/lib/crawl/types"; 6 8 7 9 const TIMEOUT_MS = 30_000; 10 + const MOCK_CRAWL = process.env.MOCK_CRAWL === "1"; 8 11 9 12 /** 10 13 * Race a promise against a timeout. Clears the timer when the primary promise ··· 21 24 } 22 25 23 26 export async function GET(request: NextRequest) { 27 + // Dev-only: serve mock crawl data without authentication 28 + if (MOCK_CRAWL) { 29 + const mockPath = path.resolve(process.cwd(), "data", "mock-crawl.json"); 30 + if (fs.existsSync(mockPath)) { 31 + const mock = JSON.parse(fs.readFileSync(mockPath, "utf-8")); 32 + return NextResponse.json({ ...mock, cached: true }); 33 + } 34 + } 35 + 24 36 const session = await getSession(); 25 37 if (!session) { 26 38 return NextResponse.json({ error: "Not authenticated" }, { status: 401 });