···3232 uri TEXT PRIMARY KEY,
3333 did TEXT NOT NULL,
3434 rkey TEXT NOT NULL,
3535+ -- Document fields
3536 title TEXT,
3737+ description TEXT,
3638 path TEXT,
3739 site TEXT,
3838- content TEXT, -- JSON blob
4040+ content TEXT, -- JSON blob for content union
3941 text_content TEXT,
4242+ cover_image_cid TEXT, -- CID for cover image blob
4343+ cover_image_url TEXT, -- Full URL: {pds}/xrpc/com.atproto.sync.getBlob?did={did}&cid={cid}
4444+ bsky_post_ref TEXT, -- JSON blob for strong reference {uri, cid}
4545+ tags TEXT, -- JSON array of strings
4046 published_at TEXT,
4141- view_url TEXT,
4747+ updated_at TEXT,
4848+ -- Publication fields (resolved from site at:// URI)
4949+ pub_url TEXT, -- Publication base URL
5050+ pub_name TEXT,
5151+ pub_description TEXT,
5252+ pub_icon_cid TEXT, -- CID for publication icon blob
5353+ pub_icon_url TEXT, -- Full URL to publication icon
5454+ -- Metadata
5555+ view_url TEXT, -- Constructed canonical URL (pub_url + path)
5656+ pds_endpoint TEXT, -- Cached PDS endpoint for this DID
4257 resolved_at TEXT DEFAULT (datetime('now')),
4358 stale_at TEXT -- When this record should be re-resolved
4459);
45604661CREATE INDEX IF NOT EXISTS idx_resolved_documents_rkey ON resolved_documents(rkey DESC);
4762CREATE INDEX IF NOT EXISTS idx_resolved_documents_stale ON resolved_documents(stale_at);
6363+CREATE INDEX IF NOT EXISTS idx_resolved_documents_pub_url ON resolved_documents(pub_url);
+2-1
packages/server/src/index.ts
···11import { Hono } from "hono";
22import { cors } from "hono/cors";
33import type { Bindings } from "./types";
44-import { health, webhook, feed, stats, records } from "./routes";
44+import { health, webhook, feed, stats, records, admin } from "./routes";
55import { processDocument } from "./utils";
6677const app = new Hono<{ Bindings: Bindings }>();
···1515app.route("/feed", feed);
1616app.route("/stats", stats);
1717app.route("/records", records);
1818+app.route("/admin", admin);
18191920// Legacy alias: /feed-raw -> /feed/raw
2021app.get("/feed-raw", async (c) => {
+77
packages/server/src/routes/admin.ts
···11+import { Hono } from "hono";
22+import type { Bindings } from "../types";
33+44+const admin = new Hono<{ Bindings: Bindings }>();
55+66+// Queue all documents for re-processing
77+admin.post("/resolve-all", async (c) => {
88+ try {
99+ const db = c.env.DB;
1010+ const queue = c.env.RESOLUTION_QUEUE;
1111+1212+ // Get all records from repo_records
1313+ const { results } = await db
1414+ .prepare(
1515+ `SELECT did, rkey FROM repo_records
1616+ WHERE collection = 'site.standard.document'`
1717+ )
1818+ .all<{ did: string; rkey: string }>();
1919+2020+ if (!results || results.length === 0) {
2121+ return c.json({ message: "No documents to process", queued: 0 });
2222+ }
2323+2424+ // Queue in batches of 100 (Cloudflare Queue limit)
2525+ const batchSize = 100;
2626+ let queued = 0;
2727+2828+ for (let i = 0; i < results.length; i += batchSize) {
2929+ const batch = results.slice(i, i + batchSize);
3030+ const messages = batch.map((row) => ({
3131+ body: {
3232+ did: row.did,
3333+ collection: "site.standard.document",
3434+ rkey: row.rkey,
3535+ },
3636+ }));
3737+3838+ await queue.sendBatch(messages);
3939+ queued += messages.length;
4040+ }
4141+4242+ return c.json({
4343+ message: "Documents queued for re-processing",
4444+ queued,
4545+ });
4646+ } catch (error) {
4747+ return c.json(
4848+ { error: "Failed to queue documents", details: String(error) },
4949+ 500
5050+ );
5151+ }
5252+});
5353+5454+// Mark all documents as stale (alternative - lets cron handle it)
5555+admin.post("/mark-stale", async (c) => {
5656+ try {
5757+ const db = c.env.DB;
5858+5959+ const result = await db
6060+ .prepare(
6161+ `UPDATE resolved_documents SET stale_at = datetime('now', '-1 hour')`
6262+ )
6363+ .run();
6464+6565+ return c.json({
6666+ message: "All documents marked as stale",
6767+ affected: result.meta.changes,
6868+ });
6969+ } catch (error) {
7070+ return c.json(
7171+ { error: "Failed to mark documents as stale", details: String(error) },
7272+ 500
7373+ );
7474+ }
7575+});
7676+7777+export default admin;
···33export { default as feed } from "./feed";
44export { default as stats } from "./stats";
55export { default as records } from "./records";
66+export { default as admin } from "./admin";