diff --git a/README.md b/README.md index d338cf6..3665781 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,6 @@ # deepevents.ai deepevents.ai main codebase + +## Modules + +- [`enterprise-tooling`](./enterprise-tooling) - runnable prototype for institutional dashboards, enterprise integrations, webhooks, compliance analytics, and export pipelines. diff --git a/enterprise-tooling/README.md b/enterprise-tooling/README.md new file mode 100644 index 0000000..c0d7867 --- /dev/null +++ b/enterprise-tooling/README.md @@ -0,0 +1,57 @@ +# Enterprise Tooling + +This module is a self-contained implementation for SCIBASE.AI issue #19. It models enterprise admin dashboards, institutional APIs/webhooks, and export pipelines for research organizations. + +## What It Covers + +- Organization-wide admin dashboard with project, storage, compute, submission, review, and reproducibility metrics. +- Contributor analytics with activity and collaboration signals. +- Compliance tracking for funder mandates, open-access status, reproducibility scores, and internal tags. +- Secure REST API catalog for institutional repositories, LMS, ELN, inventory, HRIS, and ORCID-style integrations. +- Webhook payloads for project publication and review events. +- Export pipelines for Zenodo, PubMed, arXiv, bioRxiv, journal, and funder-style handoffs. +- Formatting plugin metadata that preserves DOI, ORCID, citations, and version history. + +## Run Locally + +```bash +cd enterprise-tooling +npm test +npm start +``` + +Then open `http://localhost:4132`. + +## API Surface + +- `GET /api/dashboard` +- `GET /api/enterprise/analytics` +- `GET /api/enterprise/catalog` +- `GET /api/enterprise/webhook-preview` +- `GET /api/enterprise/export-preview?target=Zenodo&format=JATS` + +## Requirement Mapping + +- Admin dashboards: implemented by `buildAdminDashboard`. +- Contributor analytics, usage stats, productivity metrics, compliance tracking, and tags: returned in the dashboard payload. +- API and webhooks: implemented by `buildRestApiCatalog` and `createWebhookEvent`. +- Institutional integrations: represented by DSpace, Canvas, Benchling, and ORCID sync metadata. +- Export pipelines: implemented by `buildExportPipeline`. +- Formatting plugins and preserved metadata: represented by pipeline plugin lists and preserved DOI/ORCID/citation/version fields. + +## Verification + +```bash +npm test +node src/server.js +``` + +Optional smoke checks: + +```bash +curl -s http://localhost:4132/api/dashboard +curl -s http://localhost:4132/api/enterprise/webhook-preview +curl -s "http://localhost:4132/api/enterprise/export-preview?target=Zenodo&format=JATS" +``` + +Demo artifacts are committed under `docs/demo/`, including `dashboard.png` and `enterprise-tooling-demo.mp4`. diff --git a/enterprise-tooling/docs/demo-script.md b/enterprise-tooling/docs/demo-script.md new file mode 100644 index 0000000..0a22d99 --- /dev/null +++ b/enterprise-tooling/docs/demo-script.md @@ -0,0 +1,6 @@ +# Demo Script + +1. Run `npm test` to verify dashboard metrics, API catalog, webhooks, and export pipelines. +2. Run `npm start` and open `http://localhost:4132`. +3. Confirm the dashboard shows enterprise metrics, integrations, compliance rows, and export metadata. +4. Smoke-test `/api/enterprise/webhook-preview` and `/api/enterprise/export-preview?target=Zenodo&format=JATS`. diff --git a/enterprise-tooling/docs/demo/dashboard.png b/enterprise-tooling/docs/demo/dashboard.png new file mode 100644 index 0000000..63dd67a Binary files /dev/null and b/enterprise-tooling/docs/demo/dashboard.png differ diff --git a/enterprise-tooling/docs/demo/enterprise-tooling-demo.mp4 b/enterprise-tooling/docs/demo/enterprise-tooling-demo.mp4 new file mode 100644 index 0000000..29ab051 Binary files /dev/null and b/enterprise-tooling/docs/demo/enterprise-tooling-demo.mp4 differ diff --git a/enterprise-tooling/package.json b/enterprise-tooling/package.json new file mode 100644 index 0000000..0d3ca7c --- /dev/null +++ b/enterprise-tooling/package.json @@ -0,0 +1,14 @@ +{ + "name": "@scibase/enterprise-tooling", + "version": "0.1.0", + "private": true, + "description": "Self-contained enterprise tooling prototype for SCIBASE.AI issue #19.", + "type": "module", + "scripts": { + "start": "node src/server.js", + "test": "node --test test/*.test.js" + }, + "engines": { + "node": ">=20" + } +} diff --git a/enterprise-tooling/public/app.js b/enterprise-tooling/public/app.js new file mode 100644 index 0000000..1120354 --- /dev/null +++ b/enterprise-tooling/public/app.js @@ -0,0 +1,28 @@ +const payload = await fetch("/api/dashboard").then((response) => response.json()); + +document.querySelector("#orgName").textContent = payload.dashboard.organization; +document.querySelector("#overview").innerHTML = [ + row("Projects", payload.dashboard.overview.projects), + row("Storage", `${payload.dashboard.overview.storageGb} GB`), + row("Compute", `${payload.dashboard.overview.computeHours} hours`), + row("Average reproducibility", payload.dashboard.overview.averageReproducibility) +].join(""); + +document.querySelector("#api").innerHTML = payload.api.integrations + .map((integration) => `
${integration.name}${integration.type} · ${integration.enabled ? "enabled" : "disabled"}
`) + .join(""); + +document.querySelector("#compliance").innerHTML = payload.dashboard.compliance + .map((item) => `
${item.projectId}${item.mandate} · ${item.status} · score ${item.reproducibilityScore}
`) + .join(""); + +document.querySelector("#export").innerHTML = [ + row("Target", payload.exportPipeline.target), + row("Format", payload.exportPipeline.format), + row("Compliance", payload.exportPipeline.complianceStatus), + row("Preserves", payload.exportPipeline.preservedMetadata.join(", ")) +].join(""); + +function row(label, value) { + return `
${label}${String(value)}
`; +} diff --git a/enterprise-tooling/public/index.html b/enterprise-tooling/public/index.html new file mode 100644 index 0000000..c1d416e --- /dev/null +++ b/enterprise-tooling/public/index.html @@ -0,0 +1,40 @@ + + + + + + SCIBASE Enterprise Tooling + + + +
+
+

SCIBASE.AI / issue #19

+

Enterprise Tooling

+
+
+
+

Admin Dashboard

+

Loading...

+
+
+
+

API & Webhooks

+

Institutional integrations

+
+
+
+

Compliance

+

Funder and open-access status

+
+
+
+

Export Pipeline

+

Publication handoff

+
+
+
+
+ + + diff --git a/enterprise-tooling/public/styles.css b/enterprise-tooling/public/styles.css new file mode 100644 index 0000000..997428b --- /dev/null +++ b/enterprise-tooling/public/styles.css @@ -0,0 +1,110 @@ +:root { + --ink: #141816; + --muted: #63706b; + --line: #d8dfda; + --paper: #f5f4ed; + --panel: #ffffff; + --green: #17664c; + --blue: #285f8e; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--paper); + color: var(--ink); + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; +} + +.shell { + max-width: 1280px; + margin: 0 auto; + padding: 32px; +} + +header { + display: flex; + align-items: end; + justify-content: space-between; + gap: 24px; + margin-bottom: 24px; +} + +p, +h1, +h2 { + margin: 0; +} + +header p, +.label { + color: var(--muted); + font-size: 12px; + font-weight: 900; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +h1 { + max-width: 760px; + font-size: clamp(38px, 6vw, 76px); + line-height: 0.95; +} + +h2 { + margin-top: 8px; + font-size: 24px; +} + +.grid { + display: grid; + grid-template-columns: 1.05fr 0.95fr; + gap: 16px; +} + +.panel { + min-height: 260px; + border: 1px solid var(--line); + background: var(--panel); + padding: 24px; +} + +.hero { + grid-row: span 2; +} + +.row { + border-top: 1px solid var(--line); + padding-top: 12px; + margin-top: 14px; +} + +.row span { + display: block; + color: var(--muted); +} + +.row strong { + display: block; + color: var(--green); + overflow-wrap: anywhere; +} + +.hero .row strong { + color: var(--blue); +} + +@media (max-width: 820px) { + .shell { + padding: 18px; + } + + header, + .grid { + display: grid; + grid-template-columns: 1fr; + } +} diff --git a/enterprise-tooling/src/enterprise-core.js b/enterprise-tooling/src/enterprise-core.js new file mode 100644 index 0000000..6ef622e --- /dev/null +++ b/enterprise-tooling/src/enterprise-core.js @@ -0,0 +1,138 @@ +export const organization = { + id: "org-university-research", + name: "University Research Office", + departments: ["Oncology", "Neuroscience", "Materials Science"], + projects: [ + { id: "proj-organoid", title: "Organoid response atlas", department: "Oncology", visibility: "public", storageGb: 42, computeHours: 18, submissions: 3, aiReviews: 7, peerReviews: 5, reproducibilityScore: 94, openAccess: true, funderMandate: "NIH", tags: ["GRANT-TRACKED"] }, + { id: "proj-neuro", title: "CRISPR neural clustering", department: "Neuroscience", visibility: "private", storageGb: 28, computeHours: 31, submissions: 1, aiReviews: 4, peerReviews: 2, reproducibilityScore: 87, openAccess: false, funderMandate: "Horizon EU", tags: ["DOCTORAL WORK"] }, + { id: "proj-materials", title: "Polymer catalyst screening", department: "Materials Science", visibility: "institutional", storageGb: 64, computeHours: 44, submissions: 2, aiReviews: 6, peerReviews: 4, reproducibilityScore: 91, openAccess: true, funderMandate: "UKRI", tags: ["INDUSTRY"] } + ], + contributors: [ + { id: "user-alice", name: "Alice Chen", lab: "Oncology", logins: 42, commits: 18, collaborations: ["Neuroscience"], orcid: "0000-0002-1825-0097" }, + { id: "user-mateo", name: "Mateo Rivera", lab: "Open Reproducibility", logins: 33, commits: 21, collaborations: ["Oncology", "Materials Science"], orcid: "0000-0003-2201-4120" }, + { id: "user-sam", name: "Sam Okafor", lab: "Materials Science", logins: 24, commits: 11, collaborations: ["Oncology"], orcid: "0000-0001-9911-4412" } + ], + integrations: [ + { id: "dspace", name: "DSpace", type: "institutional-repository", endpoint: "https://repo.example/api", enabled: true }, + { id: "canvas", name: "Canvas", type: "lms", endpoint: "https://canvas.example/api", enabled: true }, + { id: "benchling", name: "Benchling", type: "eln", endpoint: "https://benchling.example/api", enabled: false }, + { id: "orcid", name: "ORCID Sync", type: "personnel", endpoint: "https://orcid.org/api", enabled: true } + ] +}; + +export function buildAdminDashboard(org = organization) { + const totals = org.projects.reduce( + (acc, project) => { + acc.projects += 1; + acc.storageGb += project.storageGb; + acc.computeHours += project.computeHours; + acc.submissions += project.submissions; + acc.aiReviews += project.aiReviews; + acc.peerReviews += project.peerReviews; + acc.publicProjects += project.visibility === "public" ? 1 : 0; + acc.openAccess += project.openAccess ? 1 : 0; + acc.reproducibility += project.reproducibilityScore; + return acc; + }, + { projects: 0, storageGb: 0, computeHours: 0, submissions: 0, aiReviews: 0, peerReviews: 0, publicProjects: 0, openAccess: 0, reproducibility: 0 } + ); + return { + organization: org.name, + overview: { ...totals, averageReproducibility: Math.round(totals.reproducibility / totals.projects) }, + departmentMetrics: org.departments.map((department) => summarizeDepartment(org, department)), + contributorAnalytics: org.contributors.map((contributor) => ({ + ...contributor, + productivityScore: contributor.commits * 2 + contributor.logins + contributor.collaborations.length * 8 + })), + compliance: org.projects.map((project) => ({ + projectId: project.id, + mandate: project.funderMandate, + openAccess: project.openAccess, + reproducibilityScore: project.reproducibilityScore, + status: project.openAccess && project.reproducibilityScore >= 90 ? "compliant" : "attention" + })), + tags: [...new Set(org.projects.flatMap((project) => project.tags))] + }; +} + +export function buildRestApiCatalog(org = organization) { + return { + auth: "Bearer token scoped to institution", + routes: [ + "GET /api/enterprise/projects", + "GET /api/enterprise/contributors", + "GET /api/enterprise/compliance", + "POST /api/enterprise/webhooks", + "POST /api/enterprise/exports" + ], + integrations: org.integrations, + scopes: ["read:projects", "read:analytics", "write:webhooks", "write:exports"] + }; +} + +export function createWebhookEvent(type, projectId, org = organization) { + const project = org.projects.find((item) => item.id === projectId); + if (!project) throw new Error(`Unknown project: ${projectId}`); + return { + id: `evt-${type}-${projectId}`, + type, + createdAt: "2026-05-09T00:00:00.000Z", + deliveryTargets: org.integrations.filter((integration) => integration.enabled).map((integration) => integration.endpoint), + payload: { + projectId, + title: project.title, + department: project.department, + visibility: project.visibility, + reproducibilityScore: project.reproducibilityScore, + tags: project.tags + } + }; +} + +export function buildExportPipeline({ projectId, target = "Zenodo", format = "JATS" }, org = organization) { + const project = org.projects.find((item) => item.id === projectId); + if (!project) throw new Error(`Unknown project: ${projectId}`); + const plugins = { + arXiv: ["LaTeX", "BibTeX", "PDF"], + bioRxiv: ["Docx", "JATS", "Figures"], + PubMed: ["JATS", "NIH metadata", "ORCID"], + Zenodo: ["DataCite DOI", "metadata.json", "version history"], + "NIH RePORTER": ["grant metadata", "public access compliance"] + }; + return { + projectId, + target, + format, + steps: [ + "validate project metadata", + `render manuscript as ${format}`, + "attach DOI, ORCID, citations, and version history", + `package repository for ${target}`, + "emit export.completed webhook" + ], + preservedMetadata: ["DOI", "ORCID", "citations", "version history", "funder mandate"], + plugins: plugins[target] || ["Docx", "metadata"], + complianceStatus: project.openAccess ? "ready" : "requires open-access review" + }; +} + +export function buildEnterprisePayload() { + return { + dashboard: buildAdminDashboard(), + api: buildRestApiCatalog(), + webhook: createWebhookEvent("project.published", "proj-organoid"), + exportPipeline: buildExportPipeline({ projectId: "proj-organoid", target: "Zenodo", format: "JATS" }) + }; +} + +function summarizeDepartment(org, department) { + const projects = org.projects.filter((project) => project.department === department); + return { + department, + projectCount: projects.length, + computeHours: projects.reduce((sum, project) => sum + project.computeHours, 0), + storageGb: projects.reduce((sum, project) => sum + project.storageGb, 0), + aiReviews: projects.reduce((sum, project) => sum + project.aiReviews, 0), + peerReviews: projects.reduce((sum, project) => sum + project.peerReviews, 0) + }; +} diff --git a/enterprise-tooling/src/server.js b/enterprise-tooling/src/server.js new file mode 100644 index 0000000..0cb8b2b --- /dev/null +++ b/enterprise-tooling/src/server.js @@ -0,0 +1,54 @@ +import http from "node:http"; +import { readFile } from "node:fs/promises"; +import { extname, join, resolve, sep } from "node:path"; +import { fileURLToPath } from "node:url"; +import { buildAdminDashboard, buildEnterprisePayload, buildExportPipeline, buildRestApiCatalog, createWebhookEvent } from "./enterprise-core.js"; + +const root = join(fileURLToPath(new URL("..", import.meta.url)), "public"); +const port = Number(process.env.PORT || 4132); +const contentTypes = { ".html": "text/html; charset=utf-8", ".css": "text/css; charset=utf-8", ".js": "text/javascript; charset=utf-8" }; + +const server = http.createServer(async (request, response) => { + try { + const url = new URL(request.url, `http://${request.headers.host}`); + if (url.pathname === "/api/dashboard") return json(response, buildEnterprisePayload()); + if (url.pathname === "/api/enterprise/analytics") return json(response, buildAdminDashboard()); + if (url.pathname === "/api/enterprise/catalog") return json(response, buildRestApiCatalog()); + if (url.pathname === "/api/enterprise/webhook-preview") return json(response, createWebhookEvent("project.published", "proj-organoid")); + if (url.pathname === "/api/enterprise/export-preview") return json(response, buildExportPipeline({ projectId: "proj-organoid", target: url.searchParams.get("target") || "Zenodo", format: url.searchParams.get("format") || "JATS" })); + return await serveStatic(url.pathname === "/" ? "/index.html" : url.pathname, response); + } catch (error) { + response.writeHead(500, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify({ error: error.message })); + } +}); + +server.listen(port, () => { + console.log(`Enterprise tooling demo running at http://localhost:${port}`); +}); + +function json(response, body) { + response.writeHead(200, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(body, null, 2)); +} + +async function serveStatic(pathname, response) { + const filePath = resolve(root, pathname.replace(/^\/+/, "")); + if (!filePath.startsWith(`${root}${sep}`)) { + response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }); + response.end("Forbidden"); + return; + } + try { + const body = await readFile(filePath); + response.writeHead(200, { "content-type": contentTypes[extname(filePath)] || "application/octet-stream" }); + response.end(body); + } catch (error) { + if (error.code === "ENOENT") { + response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); + response.end("Not found"); + return; + } + throw error; + } +} diff --git a/enterprise-tooling/test/enterprise-core.test.js b/enterprise-tooling/test/enterprise-core.test.js new file mode 100644 index 0000000..021bad0 --- /dev/null +++ b/enterprise-tooling/test/enterprise-core.test.js @@ -0,0 +1,37 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { buildAdminDashboard, buildEnterprisePayload, buildExportPipeline, buildRestApiCatalog, createWebhookEvent } from "../src/enterprise-core.js"; + +test("builds enterprise admin dashboard metrics", () => { + const dashboard = buildAdminDashboard(); + + assert.equal(dashboard.overview.projects, 3); + assert.ok(dashboard.overview.storageGb > 100); + assert.ok(dashboard.departmentMetrics.some((item) => item.department === "Oncology")); + assert.ok(dashboard.compliance.some((item) => item.status === "attention")); +}); + +test("exposes secure API catalog and enabled integrations", () => { + const catalog = buildRestApiCatalog(); + + assert.match(catalog.auth, /Bearer token/); + assert.ok(catalog.routes.includes("POST /api/enterprise/webhooks")); + assert.ok(catalog.integrations.some((integration) => integration.type === "lms")); +}); + +test("creates structured webhook events", () => { + const event = createWebhookEvent("project.published", "proj-organoid"); + + assert.equal(event.type, "project.published"); + assert.ok(event.deliveryTargets.length >= 2); + assert.equal(event.payload.reproducibilityScore, 94); +}); + +test("builds export pipeline preserving scholarly metadata", () => { + const pipeline = buildExportPipeline({ projectId: "proj-organoid", target: "Zenodo", format: "JATS" }); + const payload = buildEnterprisePayload(); + + assert.ok(pipeline.plugins.includes("DataCite DOI")); + assert.ok(pipeline.preservedMetadata.includes("ORCID")); + assert.equal(payload.exportPipeline.complianceStatus, "ready"); +});