Skip to content

Commit b1db331

Browse files
Your Namecursoragent
andcommitted
Ship MiniMax-M3 default, daily observability email, and production safety gates.
Make MiniMax-M3 the public LLM default, add secret-scan/config health checks to CI, and wire a cron daily intelligence report for admin observability. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 431240a commit b1db331

22 files changed

Lines changed: 1417 additions & 16 deletions

File tree

.env.example

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ MINIMAX_API_KEY=
1414
# https://api.minimax.chat/v1 (MiniMax China)
1515
# https://api.cometapi.com/v1 (CometAPI gateway)
1616
MINIMAX_BASE_URL=https://api.minimaxi.chat/v1
17-
MINIMAX_MODEL=MiniMax-Text-01
17+
# Recommended public default: MiniMax-M3 (also MiniMax-M2.7 / MiniMax-M2.7-highspeed).
18+
MINIMAX_MODEL=MiniMax-M3
1819

1920
# --- Anthropic (optional) ---
2021
# https://console.anthropic.com/
@@ -98,6 +99,18 @@ ANALYTICS_INTERNAL_SECRET=
9899
# Optional salt for IP/session hashing (defaults to a built-in value).
99100
ANALYTICS_IP_SALT=
100101

102+
# --- Daily observability email (Vercel Cron → kubee3302@gmail.com) ---
103+
# Create a free API key at https://resend.com — required to actually send mail.
104+
RESEND_API_KEY=
105+
# Verified sender in Resend (defaults to Resend onboarding address for testing).
106+
OBSERVABILITY_REPORT_FROM=Aletheia Observability <onboarding@resend.dev>
107+
# Comma-separated recipients (defaults to OWNER_EMAIL / kubee3302@gmail.com).
108+
OBSERVABILITY_REPORT_EMAIL=kubee3302@gmail.com
109+
# Set to false to pause automatic emails without removing the cron.
110+
OBSERVABILITY_DAILY_EMAIL_ENABLED=true
111+
# Protects /api/cron/daily-observability-report (also used by Vercel Cron).
112+
CRON_SECRET=
113+
101114
# Comma-separated origins for CORS if splitting API later
102115
ALLOWED_ORIGINS=http://localhost:3000,https://medcore-research-builder.vercel.app
103116

.github/workflows/ci.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ on:
66
pull_request:
77
branches: [main]
88

9+
concurrency:
10+
group: ci-${{ github.workflow }}-${{ github.ref }}
11+
cancel-in-progress: true
12+
13+
permissions:
14+
contents: read
15+
916
jobs:
1017
build:
1118
runs-on: ubuntu-latest
19+
timeout-minutes: 25
1220
steps:
1321
- uses: actions/checkout@v4
1422

@@ -19,19 +27,29 @@ jobs:
1927

2028
- run: npm ci
2129

30+
- name: Secret scan (tracked files)
31+
run: npm run secret-scan
32+
2233
- run: npm run typecheck
2334

2435
- run: npm run lint
2536

37+
- name: Unit tests
38+
run: npm run test:unit
39+
2640
- run: npm run build
2741
env:
2842
# Build succeeds without real keys; routes are lazy at runtime.
2943
MINIMAX_API_KEY: ci-placeholder
44+
MINIMAX_MODEL: MiniMax-M3
3045

3146
- name: Engine E2E (offline, no server)
3247
run: npm run test:engines
3348

3449
- name: Start server and smoke test
50+
env:
51+
MINIMAX_API_KEY: ci-placeholder
52+
MINIMAX_MODEL: MiniMax-M3
3553
run: |
3654
npm start &
3755
for i in $(seq 1 30); do
@@ -40,3 +58,7 @@ jobs:
4058
done
4159
BASE_URL=http://localhost:3000 npm run test:smoke
4260
BASE_URL=http://localhost:3000 npm run test:auth
61+
62+
- name: Dependency audit (high+ only)
63+
run: npm audit --audit-level=high
64+
continue-on-error: true

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ Publication Club, Library Navigator (363 entries), first-party MCP server, and t
6868
| Agent engine | Stateless `/api/pipeline/*` ops + client-owned run state |
6969
| In-browser stats | Pyodide (WebAssembly Python) |
7070
| State | Browser `localStorage` (optional Supabase cloud sync) |
71-
| LLM providers | MiniMax (default), Anthropic, OpenAI — routed by task complexity |
71+
| LLM providers | **MiniMax-M3** (default), Anthropic, OpenAI — routed by task complexity |
7272
| Export | DOCX, Markdown, CSV, JSON, HTML (poster/slides/graphical abstract), PY |
7373
| Deploy | `output: "standalone"` — Vercel, Railway, or any Node host |
7474

@@ -91,7 +91,9 @@ Pyodide analysis lab still work.
9191
| Variable | Required | Purpose |
9292
|----------|----------|---------|
9393
| `LLM_PROVIDER` | optional | `minimax` (default), `anthropic`, or `openai` |
94-
| `MINIMAX_API_KEY` | recommended | Default LLM provider |
94+
| `MINIMAX_API_KEY` | recommended | Default LLM provider (MiniMax-M3) |
95+
| `MINIMAX_MODEL` | optional | Default `MiniMax-M3` |
96+
| `MINIMAX_BASE_URL` | optional | Default `https://api.minimaxi.chat/v1` |
9597
| `ANTHROPIC_API_KEY` | optional | Anthropic LLM |
9698
| `OPENAI_API_KEY` | optional | OpenAI fallback |
9799
| `LLM_MODEL_LIGHT` / `LLM_MODEL_STANDARD` / `LLM_MODEL_DEEP` | optional | Override the cost router's model per complexity tier |

app/admin/observability/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,9 @@ export default async function AdminObservabilityPage() {
4949
<div className="mb-6">
5050
<h1 className="display-title text-2xl">Observability</h1>
5151
<p className="muted text-sm mt-1 max-w-2xl">
52-
Visits, auth activity, feature usage, abuse signals, and alerts for open-beta safety and
53-
marketing insights. No raw IPs or user emails are shown in aggregates.
52+
Visits, auth activity, feature usage, abuse signals, daily intelligence reports, and
53+
alerts for open-beta safety and marketing insights. No raw IPs or user emails are shown
54+
in aggregates.
5455
</p>
5556
</div>
5657
<ObservabilityDashboard />
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { ok, bad, handleError, enforceRateLimit } from "../../../_utils";
2+
import { getAppUser } from "@/lib/auth";
3+
import { buildDailyIntelligenceReport, formatReportPlainText } from "@/lib/analytics/dailyReport";
4+
import { sendDailyObservabilityEmail } from "@/lib/analytics/emailReport";
5+
6+
export const runtime = "nodejs";
7+
export const dynamic = "force-dynamic";
8+
export const maxDuration = 60;
9+
10+
/** Admin: view or email the daily intelligence report. */
11+
export async function GET(req: Request) {
12+
try {
13+
const limited = await enforceRateLimit(req, "default");
14+
if (limited) return limited;
15+
16+
const user = await getAppUser();
17+
if (!user || user.role !== "admin") return bad("Admin access required", 403);
18+
19+
const url = new URL(req.url);
20+
const days = Math.min(90, Math.max(7, Number(url.searchParams.get("days") || 30)));
21+
const format = (url.searchParams.get("format") || "json").toLowerCase();
22+
const report = await buildDailyIntelligenceReport(days);
23+
24+
if (format === "text" || format === "txt") {
25+
return new Response(formatReportPlainText(report), {
26+
headers: {
27+
"Content-Type": "text/plain; charset=utf-8",
28+
"Cache-Control": "no-store",
29+
},
30+
});
31+
}
32+
33+
return ok({
34+
report,
35+
email: {
36+
configured: Boolean(process.env.RESEND_API_KEY),
37+
enabled: process.env.OBSERVABILITY_DAILY_EMAIL_ENABLED !== "false",
38+
to:
39+
process.env.OBSERVABILITY_REPORT_EMAIL ||
40+
process.env.OWNER_EMAIL ||
41+
"kubee3302@gmail.com",
42+
schedule: "0 6 * * * (06:00 UTC daily via Vercel Cron)",
43+
},
44+
admin: { email: user.email },
45+
});
46+
} catch (e) {
47+
return handleError(e);
48+
}
49+
}
50+
51+
/** Admin: send the daily report email now. */
52+
export async function POST(req: Request) {
53+
try {
54+
const limited = await enforceRateLimit(req, "default");
55+
if (limited) return limited;
56+
57+
const user = await getAppUser();
58+
if (!user || user.role !== "admin") return bad("Admin access required", 403);
59+
60+
const body = (await req.json().catch(() => ({}))) as { days?: number };
61+
const days = Math.min(90, Math.max(7, Number(body.days || 30)));
62+
const result = await sendDailyObservabilityEmail({ rangeDays: days, force: true });
63+
64+
return ok({
65+
ok: result.ok,
66+
skipped: result.skipped ?? false,
67+
reason: result.reason,
68+
to: result.to,
69+
subject: result.subject,
70+
providerId: result.providerId,
71+
headline: result.report.headline,
72+
reportDate: result.report.reportDate,
73+
});
74+
} catch (e) {
75+
return handleError(e);
76+
}
77+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { NextResponse } from "next/server";
2+
import { sendDailyObservabilityEmail } from "@/lib/analytics/emailReport";
3+
4+
export const runtime = "nodejs";
5+
export const dynamic = "force-dynamic";
6+
export const maxDuration = 60;
7+
8+
/**
9+
* Vercel Cron: daily observability email.
10+
* Secured by CRON_SECRET (Authorization: Bearer <secret>) or Vercel cron header.
11+
*/
12+
export async function GET(req: Request) {
13+
const auth = req.headers.get("authorization") || "";
14+
const cronSecret = process.env.CRON_SECRET || "";
15+
const vercelCron = req.headers.get("x-vercel-cron");
16+
const okAuth =
17+
(cronSecret && auth === `Bearer ${cronSecret}`) ||
18+
(!!vercelCron && process.env.VERCEL === "1");
19+
20+
// Also allow CRON_SECRET as query for manual ops (optional)
21+
const url = new URL(req.url);
22+
const qSecret = url.searchParams.get("secret");
23+
const okQuery = !!(cronSecret && qSecret && qSecret === cronSecret);
24+
25+
if (!okAuth && !okQuery) {
26+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
27+
}
28+
29+
if (process.env.OBSERVABILITY_DAILY_EMAIL_ENABLED === "false") {
30+
return NextResponse.json({
31+
ok: false,
32+
skipped: true,
33+
reason: "OBSERVABILITY_DAILY_EMAIL_ENABLED=false",
34+
});
35+
}
36+
37+
const days = Math.min(90, Math.max(7, Number(url.searchParams.get("days") || 30)));
38+
const result = await sendDailyObservabilityEmail({ rangeDays: days });
39+
40+
return NextResponse.json({
41+
ok: result.ok,
42+
skipped: result.skipped ?? false,
43+
reason: result.reason,
44+
to: result.to,
45+
subject: result.subject,
46+
providerId: result.providerId,
47+
headline: result.report.headline,
48+
reportDate: result.report.reportDate,
49+
generatedAt: result.report.generatedAt,
50+
});
51+
}
52+
53+
export async function POST(req: Request) {
54+
return GET(req);
55+
}

app/api/status/route.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,29 @@ import { elicitConfigured } from "@/lib/scholarly/elicit";
77
import { unpaywallConfigured } from "@/lib/scholarly/unpaywall";
88
import { isSupabaseConfigured } from "@/lib/supabase/server";
99
import { rateLimitBackend } from "@/lib/rateLimit";
10+
import { validateRuntimeConfig } from "@/lib/config/validate";
1011

1112
export const runtime = "nodejs";
1213

1314
export async function GET() {
15+
const config = validateRuntimeConfig();
1416
return ok({
1517
version: APP_VERSION,
18+
ready: config.ok,
19+
config: {
20+
ok: config.ok,
21+
// Names + boolean status only — never values.
22+
checks: config.checks.map((c) => ({
23+
name: c.name,
24+
ok: c.ok,
25+
required: c.required,
26+
detail: c.detail,
27+
})),
28+
},
1629
llm: {
1730
configured: isLLMConfigured(),
1831
provider: getActiveProvider(),
32+
modelHint: process.env.MINIMAX_MODEL || "MiniMax-M3",
1933
},
2034
pubmed: {
2135
// PubMed works without a key but with stricter rate limits.

0 commit comments

Comments
 (0)