MapleBrief: production Canadian news aggregator

Next.js 14 + TypeScript + Tailwind + Prisma/SQLite.
- LLM synthesis abstraction (OpenAI/Anthropic/Ollama) + admin settings UI
- RSS ingestion pipeline (parser + cheerio content) + node-cron worker
- AdSense AdUnit placements (header/in-feed/in-article/sidebar)
- Sources analysis attribution, nofollow links, canonical/OG, sitemap/robots
- Admin auth (ADMIN_API_KEY, timingSafeEqual, fail-closed 401)
- Multi-cell lady-ga-ga marker, sitemap, robots, legal pages
- Comprehensive README (setup, LLM config, AdSense, migrations, deploy)
This commit is contained in:
2026-08-15 16:07:07 -04:00
commit 1010f27f44
76 changed files with 7177 additions and 0 deletions
+138
View File
@@ -0,0 +1,138 @@
import { NextRequest, NextResponse } from 'next/server';
import { isAuthorized } from '@/lib/auth/admin';
import { prisma } from '@/lib/db';
import {
getLlmSettings,
upsertSetting,
SETTING_KEYS,
} from '@/lib/llm';
import type { LlmProviderId } from '@/lib/settings';
export const runtime = 'nodejs';
export const dynamic = 'force-dynamic';
const PROVIDERS: LlmProviderId[] = ['openai', 'anthropic', 'ollama'];
const PRO_MODEL_KEYS: Record<LlmProviderId, string> = {
openai: 'llm.model.openai',
anthropic: 'llm.model.anthropic',
ollama: 'llm.model.ollama',
};
/** Last-4 for display only — full secrets never leave the server. */
function last4(secret: string): string {
return secret.length >= 4 ? `${secret.slice(-4)}` : '';
}
function isProvider(v: unknown): v is LlmProviderId {
return v === 'openai' || v === 'anthropic' || v === 'ollama';
}
async function readSetting(key: string): Promise<string | null> {
const row = await prisma.setting.findUnique({ where: { key } });
return row?.value ?? null;
}
interface SettingsBody {
provider?: unknown;
model?: unknown; // legacy single-model field
models?: Partial<Record<LlmProviderId, unknown>>;
apiKeys?: { openai?: unknown; anthropic?: unknown };
ollamaBase?: unknown;
synthesisPrompt?: unknown;
}
async function toResponseBody() {
const s = await getLlmSettings();
const [dbProvider, dbModel, dbOpenai, dbAnthropic] = await Promise.all([
readSetting(SETTING_KEYS.provider),
readSetting(SETTING_KEYS.model),
readSetting(SETTING_KEYS.openaiKey),
readSetting(SETTING_KEYS.anthropicKey),
]);
const models = {} as Record<LlmProviderId, string>;
for (const p of PROVIDERS) models[p] = (await readSetting(PRO_MODEL_KEYS[p])) ?? s.model;
// A legacy single-model override applies to the provider it was set for.
const legacyOwner = isProvider(dbProvider) ? dbProvider : 'openai';
if (dbModel) models[legacyOwner] = dbModel;
return {
settings: {
provider: s.provider,
models,
apiKeys: { openai: '', anthropic: '' }, // secrets are never echoed
ollamaBase: s.ollamaBaseUrl,
synthesisPrompt: s.synthesisPrompt,
},
keySources: {
openai: Boolean(dbOpenai),
anthropic: Boolean(dbAnthropic),
ollama: true,
},
keys: {
openaiLast4: last4(s.openaiApiKey),
anthropicLast4: last4(s.anthropicApiKey),
},
};
}
export async function GET(req: NextRequest) {
if (!isAuthorized(req)) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
return NextResponse.json(await toResponseBody());
}
export async function PUT(req: NextRequest) {
if (!isAuthorized(req)) {
return NextResponse.json({ error: 'unauthorized' }, { status: 401 });
}
const body = (await req.json().catch(() => null)) as SettingsBody | null;
if (!body) return NextResponse.json({ error: 'invalid json' }, { status: 400 });
// Provider
if (body.provider !== undefined) {
if (!isProvider(body.provider)) {
return NextResponse.json(
{ error: 'provider must be openai | anthropic | ollama' },
{ status: 400 },
);
}
await upsertSetting(SETTING_KEYS.provider, body.provider);
}
const active =
(body.provider as LlmProviderId | undefined) ?? (await getLlmSettings()).provider;
// Models — per-provider, with single-model fallback for simple clients
if (body.models && typeof body.models === 'object') {
for (const p of PROVIDERS) {
const m = String(body.models[p] ?? '').trim();
if (m) {
await upsertSetting(PRO_MODEL_KEYS[p], m);
if (p === active) await upsertSetting(SETTING_KEYS.model, m);
}
}
} else if (typeof body.model === 'string' && body.model.trim()) {
await upsertSetting(SETTING_KEYS.model, body.model.trim());
await upsertSetting(PRO_MODEL_KEYS[active], body.model.trim());
}
// API keys (secrets; blank from the UI means "keep using the env var")
if (body.apiKeys) {
const openaiKey = String(body.apiKeys.openai ?? '').trim();
const anthropicKey = String(body.apiKeys.anthropic ?? '').trim();
if (openaiKey) await upsertSetting(SETTING_KEYS.openaiKey, openaiKey, true);
if (anthropicKey) await upsertSetting(SETTING_KEYS.anthropicKey, anthropicKey, true);
}
// Ollama base URL
const ollamaBase = String(body.ollamaBase ?? '').trim();
if (ollamaBase) await upsertSetting(SETTING_KEYS.ollamaBase, ollamaBase);
// Synthesis prompt
const prompt = String(body.synthesisPrompt ?? '').trim();
if (prompt) await upsertSetting(SETTING_KEYS.synthesisPrompt, prompt);
return NextResponse.json(await toResponseBody());
}