···11+import { serve } from "@hono/node-server";
22+import { openDb, migrate } from "./db.js";
33+import { createRoutes } from "./routes.js";
44+55+const db = openDb();
66+migrate(db);
77+88+const app = createRoutes(db);
99+1010+const port = parseInt(process.env.PORT || "3001", 10);
1111+serve({ fetch: app.fetch, port }, (info) => {
1212+ console.log(`Ionosphere appview running on http://localhost:${info.port}`);
1313+});
+112
apps/ionosphere-appview/src/db.ts
···11+import Database from "better-sqlite3";
22+import path from "node:path";
33+44+const DB_PATH = path.resolve(
55+ import.meta.dirname,
66+ "../../data/ionosphere.sqlite"
77+);
88+99+export function openDb(): Database.Database {
1010+ const db = new Database(DB_PATH);
1111+ db.pragma("journal_mode = WAL");
1212+ db.pragma("foreign_keys = ON");
1313+ return db;
1414+}
1515+1616+export function migrate(db: Database.Database): void {
1717+ db.exec(`
1818+ CREATE TABLE IF NOT EXISTS events (
1919+ uri TEXT PRIMARY KEY,
2020+ did TEXT NOT NULL,
2121+ rkey TEXT NOT NULL,
2222+ name TEXT NOT NULL,
2323+ description TEXT,
2424+ location TEXT,
2525+ starts_at TEXT NOT NULL,
2626+ ends_at TEXT NOT NULL,
2727+ tracks TEXT,
2828+ schedule_repo TEXT,
2929+ vod_repo TEXT,
3030+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
3131+ );
3232+3333+ CREATE TABLE IF NOT EXISTS speakers (
3434+ uri TEXT PRIMARY KEY,
3535+ did TEXT,
3636+ rkey TEXT NOT NULL,
3737+ name TEXT NOT NULL,
3838+ handle TEXT,
3939+ speaker_did TEXT,
4040+ bio TEXT,
4141+ affiliations TEXT,
4242+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
4343+ );
4444+4545+ CREATE TABLE IF NOT EXISTS talks (
4646+ uri TEXT PRIMARY KEY,
4747+ did TEXT NOT NULL,
4848+ rkey TEXT NOT NULL,
4949+ title TEXT NOT NULL,
5050+ description TEXT,
5151+ document TEXT,
5252+ video_uri TEXT,
5353+ schedule_uri TEXT,
5454+ event_uri TEXT NOT NULL,
5555+ room TEXT,
5656+ category TEXT,
5757+ talk_type TEXT,
5858+ starts_at TEXT,
5959+ ends_at TEXT,
6060+ duration INTEGER,
6161+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
6262+ FOREIGN KEY (event_uri) REFERENCES events(uri) ON DELETE CASCADE
6363+ );
6464+6565+ CREATE TABLE IF NOT EXISTS talk_speakers (
6666+ talk_uri TEXT NOT NULL,
6767+ speaker_uri TEXT NOT NULL,
6868+ PRIMARY KEY (talk_uri, speaker_uri),
6969+ FOREIGN KEY (talk_uri) REFERENCES talks(uri) ON DELETE CASCADE,
7070+ FOREIGN KEY (speaker_uri) REFERENCES speakers(uri) ON DELETE CASCADE
7171+ );
7272+7373+ CREATE TABLE IF NOT EXISTS concepts (
7474+ uri TEXT PRIMARY KEY,
7575+ did TEXT NOT NULL,
7676+ rkey TEXT NOT NULL,
7777+ name TEXT NOT NULL,
7878+ aliases TEXT,
7979+ description TEXT,
8080+ wikidata_id TEXT,
8181+ url TEXT,
8282+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
8383+ );
8484+8585+ CREATE TABLE IF NOT EXISTS talk_concepts (
8686+ talk_uri TEXT NOT NULL,
8787+ concept_uri TEXT NOT NULL,
8888+ mention_count INTEGER DEFAULT 1,
8989+ PRIMARY KEY (talk_uri, concept_uri),
9090+ FOREIGN KEY (talk_uri) REFERENCES talks(uri) ON DELETE CASCADE,
9191+ FOREIGN KEY (concept_uri) REFERENCES concepts(uri) ON DELETE CASCADE
9292+ );
9393+9494+ CREATE TABLE IF NOT EXISTS talk_crossrefs (
9595+ from_talk_uri TEXT NOT NULL,
9696+ to_talk_uri TEXT NOT NULL,
9797+ PRIMARY KEY (from_talk_uri, to_talk_uri),
9898+ FOREIGN KEY (from_talk_uri) REFERENCES talks(uri) ON DELETE CASCADE,
9999+ FOREIGN KEY (to_talk_uri) REFERENCES talks(uri) ON DELETE CASCADE
100100+ );
101101+102102+ CREATE TABLE IF NOT EXISTS pipeline_status (
103103+ talk_uri TEXT PRIMARY KEY,
104104+ ingested INTEGER DEFAULT 0,
105105+ transcribed INTEGER DEFAULT 0,
106106+ assembled INTEGER DEFAULT 0,
107107+ enriched INTEGER DEFAULT 0,
108108+ updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
109109+ FOREIGN KEY (talk_uri) REFERENCES talks(uri) ON DELETE CASCADE
110110+ );
111111+ `);
112112+}
+96
apps/ionosphere-appview/src/routes.ts
···11+import { Hono } from "hono";
22+import type Database from "better-sqlite3";
33+44+export function createRoutes(db: Database.Database): Hono {
55+ const app = new Hono();
66+77+ app.get("/health", (c) => c.json({ status: "ok" }));
88+99+ app.get("/talks", (c) => {
1010+ const talks = db
1111+ .prepare(
1212+ `SELECT t.*, GROUP_CONCAT(s.name) as speaker_names
1313+ FROM talks t
1414+ LEFT JOIN talk_speakers ts ON t.uri = ts.talk_uri
1515+ LEFT JOIN speakers s ON ts.speaker_uri = s.uri
1616+ GROUP BY t.uri
1717+ ORDER BY t.starts_at ASC`
1818+ )
1919+ .all();
2020+ return c.json({ talks });
2121+ });
2222+2323+ app.get("/talks/:rkey", (c) => {
2424+ const { rkey } = c.req.param();
2525+ const talk = db
2626+ .prepare("SELECT * FROM talks WHERE rkey = ?")
2727+ .get(rkey);
2828+ if (!talk) return c.json({ error: "not found" }, 404);
2929+3030+ const speakers = db
3131+ .prepare(
3232+ `SELECT s.* FROM speakers s
3333+ JOIN talk_speakers ts ON s.uri = ts.speaker_uri
3434+ WHERE ts.talk_uri = ?`
3535+ )
3636+ .all((talk as any).uri);
3737+3838+ const concepts = db
3939+ .prepare(
4040+ `SELECT c.* FROM concepts c
4141+ JOIN talk_concepts tc ON c.uri = tc.concept_uri
4242+ WHERE tc.talk_uri = ?`
4343+ )
4444+ .all((talk as any).uri);
4545+4646+ return c.json({ talk, speakers, concepts });
4747+ });
4848+4949+ app.get("/speakers", (c) => {
5050+ const speakers = db.prepare("SELECT * FROM speakers ORDER BY name ASC").all();
5151+ return c.json({ speakers });
5252+ });
5353+5454+ app.get("/speakers/:rkey", (c) => {
5555+ const { rkey } = c.req.param();
5656+ const speaker = db.prepare("SELECT * FROM speakers WHERE rkey = ?").get(rkey);
5757+ if (!speaker) return c.json({ error: "not found" }, 404);
5858+5959+ const talks = db
6060+ .prepare(
6161+ `SELECT t.* FROM talks t
6262+ JOIN talk_speakers ts ON t.uri = ts.talk_uri
6363+ WHERE ts.speaker_uri = ?
6464+ ORDER BY t.starts_at ASC`
6565+ )
6666+ .all((speaker as any).uri);
6767+6868+ return c.json({ speaker, talks });
6969+ });
7070+7171+ app.get("/concepts", (c) => {
7272+ const concepts = db
7373+ .prepare("SELECT * FROM concepts ORDER BY name ASC")
7474+ .all();
7575+ return c.json({ concepts });
7676+ });
7777+7878+ app.get("/concepts/:rkey", (c) => {
7979+ const { rkey } = c.req.param();
8080+ const concept = db.prepare("SELECT * FROM concepts WHERE rkey = ?").get(rkey);
8181+ if (!concept) return c.json({ error: "not found" }, 404);
8282+8383+ const talks = db
8484+ .prepare(
8585+ `SELECT t.* FROM talks t
8686+ JOIN talk_concepts tc ON t.uri = tc.talk_uri
8787+ WHERE tc.concept_uri = ?
8888+ ORDER BY t.starts_at ASC`
8989+ )
9090+ .all((concept as any).uri);
9191+9292+ return c.json({ concept, talks });
9393+ });
9494+9595+ return app;
9696+}