Skip to content

Commit c549e33

Browse files
Shaun Mahonyclaude
andcommitted
Add multi-database support, CIS-BP integration, and UI improvements
- Add DatabaseSelection type and update MatchingConfig to use databases[] instead of taxonGroups[], enabling multiple reference databases per job - Add version and urlPattern fields to ReferenceDatabase model; expand source enum to include cisbp - Add dbSource and group fields to Motif model; make JASPAR-specific fields optional for cross-database compatibility - Update processor to write three-part DE lines (DBSOURCE::ID::NAME) and resolve per-database URL patterns for match result links - Add CIS-BP parser (PWM + TF_Information) and sync library that downloads directly from the CIS-BP server (Build 3.00) - Add /api/admin/sync-cisbp route and /api/databases public endpoint - Rewrite DatabaseSelector: collapsible per-source blocks, JASPAR expanded by default, vertebrates first, top matches 1/5/10/20/50 - Update admin page with CIS-BP server-sync button and database status - Fix citation footer to be an active link; update GitHub URL to seqcode/stamp; add 20 and 50 to top matches options - Bump topMatches validation max from 20 to 50 Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
1 parent 0cc6da2 commit c549e33

File tree

20 files changed

+852
-100
lines changed

20 files changed

+852
-100
lines changed

web/package-lock.json

Lines changed: 21 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,34 @@
1313
"test:watch": "vitest"
1414
},
1515
"dependencies": {
16-
"next": "^14.2.0",
17-
"react": "^18.3.0",
18-
"react-dom": "^18.3.0",
19-
"mongoose": "^8.0.0",
16+
"adm-zip": "^0.5.16",
17+
"archiver": "^7.0.0",
2018
"bullmq": "^5.0.0",
19+
"d3": "^7.9.0",
2120
"ioredis": "^5.4.0",
22-
"zod": "^3.23.0",
21+
"mongoose": "^8.0.0",
2322
"nanoid": "^5.0.0",
24-
"archiver": "^7.0.0",
23+
"next": "^14.2.0",
2524
"nodemailer": "^6.9.0",
26-
"d3": "^7.9.0"
25+
"react": "^18.3.0",
26+
"react-dom": "^18.3.0",
27+
"zod": "^3.23.0"
2728
},
2829
"devDependencies": {
30+
"@types/adm-zip": "^0.5.7",
31+
"@types/archiver": "^6.0.0",
32+
"@types/d3": "^7.4.0",
2933
"@types/node": "^22.0.0",
34+
"@types/nodemailer": "^6.4.0",
3035
"@types/react": "^18.3.0",
3136
"@types/react-dom": "^18.3.0",
32-
"@types/archiver": "^6.0.0",
33-
"@types/nodemailer": "^6.4.0",
34-
"@types/d3": "^7.4.0",
35-
"typescript": "^5.5.0",
36-
"tailwindcss": "^3.4.0",
37-
"postcss": "^8.4.0",
3837
"autoprefixer": "^10.4.0",
3938
"eslint": "^8.0.0",
4039
"eslint-config-next": "^14.2.0",
40+
"postcss": "^8.4.0",
41+
"tailwindcss": "^3.4.0",
4142
"tsx": "^4.0.0",
43+
"typescript": "^5.5.0",
4244
"vitest": "^1.6.0"
4345
},
4446
"engines": {

web/src/app/admin/page.tsx

Lines changed: 70 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface DatabaseInfo {
1818
name: string;
1919
slug: string;
2020
source: string;
21+
version: string | null;
2122
motifCount: number;
2223
taxonGroups: string[];
2324
lastSyncedAt: string | null;
@@ -32,6 +33,10 @@ export default function AdminPage() {
3233
const [selectedTaxons, setSelectedTaxons] = useState<string[]>([]);
3334
const [cleanupDays, setCleanupDays] = useState(7);
3435

36+
// CIS-BP state
37+
const [cisbpSyncing, setCisbpSyncing] = useState(false);
38+
const [cisbpResult, setCisbpResult] = useState<string | null>(null);
39+
3540
const fetchData = useCallback(async () => {
3641
const [jobsRes, dbsRes] = await Promise.all([
3742
fetch("/api/admin/jobs"),
@@ -81,6 +86,30 @@ export default function AdminPage() {
8186
}
8287
};
8388

89+
const handleCisbpSync = async () => {
90+
setCisbpSyncing(true);
91+
setCisbpResult(null);
92+
try {
93+
const res = await fetch("/api/admin/sync-cisbp", {
94+
method: "POST",
95+
headers: { "Content-Type": "application/json" },
96+
});
97+
const data = await res.json();
98+
if (res.ok) {
99+
setCisbpResult(
100+
`Sync complete: ${data.result.totalStored} motifs stored from ${data.result.species.length} species, ${data.result.errors.length} errors`
101+
);
102+
fetchData();
103+
} else {
104+
setCisbpResult(`Sync failed: ${data.error}`);
105+
}
106+
} catch (err) {
107+
setCisbpResult(`Sync failed: ${err instanceof Error ? err.message : String(err)}`);
108+
} finally {
109+
setCisbpSyncing(false);
110+
}
111+
};
112+
84113
const handleCleanup = async () => {
85114
if (!confirm(`Delete all jobs older than ${cleanupDays} days?`)) return;
86115
try {
@@ -161,11 +190,18 @@ export default function AdminPage() {
161190
className="flex items-center justify-between p-3 border border-gray-200 rounded-lg"
162191
>
163192
<div>
164-
<p className="font-medium text-sm text-gray-900">{db.name}</p>
193+
<p className="font-medium text-sm text-gray-900">
194+
{db.name}
195+
{db.version && (
196+
<span className="ml-2 text-xs text-gray-400 font-normal">
197+
v{db.version}
198+
</span>
199+
)}
200+
</p>
165201
<p className="text-xs text-gray-500">
166-
{db.motifCount} motifs
202+
{db.motifCount.toLocaleString()} motifs
167203
{db.taxonGroups.length > 0 &&
168-
` \u00B7 ${db.taxonGroups.join(", ")}`}
204+
` \u00B7 ${db.taxonGroups.length} groups`}
169205
{db.lastSyncedAt &&
170206
` \u00B7 Last synced: ${new Date(db.lastSyncedAt).toLocaleDateString()}`}
171207
</p>
@@ -184,11 +220,11 @@ export default function AdminPage() {
184220
</div>
185221
) : (
186222
<p className="text-sm text-gray-500 mb-6">
187-
No reference databases configured. Sync JASPAR to get started.
223+
No reference databases configured. Sync JASPAR or upload CIS-BP to get started.
188224
</p>
189225
)}
190226

191-
{/* Sync Controls */}
227+
{/* JASPAR Sync Controls */}
192228
<div className="border-t border-gray-200 pt-4">
193229
<h4 className="text-sm font-medium text-gray-900 mb-3">
194230
Sync JASPAR Database
@@ -220,13 +256,41 @@ export default function AdminPage() {
220256

221257
<div className="flex items-center gap-3">
222258
<Button onClick={handleSync} disabled={syncing}>
223-
{syncing ? "Syncing..." : "Start Sync"}
259+
{syncing ? "Syncing..." : "Start JASPAR Sync"}
224260
</Button>
225261
{syncResult && (
226262
<p className="text-sm text-gray-600">{syncResult}</p>
227263
)}
228264
</div>
229265
</div>
266+
267+
{/* CIS-BP Sync Controls */}
268+
<div className="border-t border-gray-200 pt-4 mt-4">
269+
<h4 className="text-sm font-medium text-gray-900 mb-3">
270+
Sync CIS-BP Database
271+
</h4>
272+
<p className="text-xs text-gray-500 mb-3">
273+
Downloads{" "}
274+
<a
275+
href="https://cisbp.ccbr.utoronto.ca/entireDownload.php"
276+
target="_blank"
277+
rel="noopener noreferrer"
278+
className="underline hover:text-gray-600"
279+
>
280+
CIS-BP Build 3.00
281+
</a>{" "}
282+
(PWMs + TF_Information) directly from the server. This replaces any
283+
existing CIS-BP data and may take a few minutes.
284+
</p>
285+
<div className="flex items-center gap-3">
286+
<Button onClick={handleCisbpSync} disabled={cisbpSyncing}>
287+
{cisbpSyncing ? "Syncing..." : "Sync CIS-BP from Server"}
288+
</Button>
289+
</div>
290+
{cisbpResult && (
291+
<p className="text-sm text-gray-600 mt-2">{cisbpResult}</p>
292+
)}
293+
</div>
230294
</Card>
231295
</div>
232296
);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { connectDB } from "@/lib/db/mongoose";
3+
import { syncCisbpFromWeb, syncCisbp } from "@/lib/cisbp/sync";
4+
5+
function isAdmin(request: NextRequest): boolean {
6+
return request.cookies.get("stamp-admin")?.value === "authenticated";
7+
}
8+
9+
export async function POST(request: NextRequest) {
10+
if (!isAdmin(request)) {
11+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
12+
}
13+
14+
try {
15+
await connectDB();
16+
17+
const contentType = request.headers.get("content-type") || "";
18+
19+
// Multipart upload (legacy/fallback path)
20+
if (contentType.includes("multipart/form-data")) {
21+
const formData = await request.formData();
22+
const file = formData.get("zipFile") as File | null;
23+
if (!file || file.size === 0) {
24+
return NextResponse.json(
25+
{ error: "No ZIP file provided." },
26+
{ status: 400 }
27+
);
28+
}
29+
const buffer = Buffer.from(await file.arrayBuffer());
30+
const result = await syncCisbp(buffer);
31+
return NextResponse.json({ success: true, result });
32+
}
33+
34+
// JSON body — download directly from CIS-BP server
35+
const result = await syncCisbpFromWeb();
36+
return NextResponse.json({ success: true, result });
37+
} catch (error) {
38+
return NextResponse.json(
39+
{
40+
error: `CIS-BP sync failed: ${
41+
error instanceof Error ? error.message : String(error)
42+
}`,
43+
},
44+
{ status: 500 }
45+
);
46+
}
47+
}

web/src/app/api/databases/route.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NextResponse } from "next/server";
2+
import { connectDB } from "@/lib/db/mongoose";
3+
import { ReferenceDatabase } from "@/lib/db/models/ReferenceDatabase";
4+
5+
export async function GET() {
6+
try {
7+
await connectDB();
8+
9+
const databases = await ReferenceDatabase.find({ isActive: true })
10+
.sort({ source: 1, name: 1 })
11+
.lean();
12+
13+
const result = databases.map((db) => ({
14+
slug: db.slug,
15+
name: db.name,
16+
source: db.source,
17+
version: db.version || null,
18+
motifCount: db.motifCount,
19+
taxonGroups: db.taxonGroups,
20+
lastSyncedAt: db.lastSyncedAt,
21+
}));
22+
23+
return NextResponse.json({ databases: result });
24+
} catch (error) {
25+
console.error("Failed to fetch databases:", error);
26+
return NextResponse.json(
27+
{ error: "Failed to fetch databases." },
28+
{ status: 500 }
29+
);
30+
}
31+
}

web/src/app/api/jobs/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export async function POST(request: NextRequest) {
8181
const params = { ...DEFAULT_PARAMS, ...validated.params };
8282
const matching = {
8383
enabled: validated.matching.enabled,
84-
taxonGroups: validated.matching.taxonGroups,
84+
databases: validated.matching.databases,
8585
topMatches: validated.matching.topMatches,
8686
customDbFileKey: validated.matching.customDbFileKey,
8787
};

web/src/app/layout.tsx

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default function RootLayout({
2929
Submit Job
3030
</a>
3131
<a
32-
href="https://github.com/shaunmahony/stamp"
32+
href="https://github.com/seqcode/stamp"
3333
target="_blank"
3434
rel="noopener noreferrer"
3535
className="hover:text-gray-900"
@@ -41,8 +41,22 @@ export default function RootLayout({
4141
</header>
4242
<main className="flex-1">{children}</main>
4343
<footer className="border-t border-gray-200 bg-gray-50 py-6 text-center text-sm text-gray-500">
44-
STAMP v2.0 &mdash; Similarity, Tree-building, &amp; Alignment of
45-
Motifs and Profiles
44+
<p>
45+
STAMP v2.0 &mdash; Similarity, Tree-building, &amp; Alignment of
46+
Motifs and Profiles
47+
</p>
48+
<p className="mt-2 text-xs text-gray-400">
49+
<a
50+
href="https://academic.oup.com/nar/article/35/suppl_2/W253/2920802"
51+
target="_blank"
52+
rel="noopener noreferrer"
53+
className="hover:text-gray-500 underline underline-offset-2"
54+
>
55+
Mahony S, Benos PV. STAMP: a web tool for exploring DNA-binding
56+
motif similarities. <em>Nucleic Acids Res.</em> 2007
57+
Jul;35(Web Server issue):W253-8.
58+
</a>
59+
</p>
4660
</footer>
4761
</div>
4862
</body>

web/src/app/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default function HomePage() {
1616
const [params, setParams] = useState<StampParams>({ ...DEFAULT_PARAMS });
1717
const [matching, setMatching] = useState<MatchingConfig>({
1818
enabled: true,
19-
taxonGroups: ["vertebrates"],
19+
databases: [{ slug: "jaspar-core", groups: ["vertebrates"] }],
2020
topMatches: 5,
2121
customDbFileKey: null,
2222
});

0 commit comments

Comments
 (0)